diff --git a/contracts/javascore/build.gradle b/contracts/javascore/build.gradle index b74617cb9..3b2c57155 100644 --- a/contracts/javascore/build.gradle +++ b/contracts/javascore/build.gradle @@ -15,6 +15,7 @@ subprojects { } apply plugin: 'java' apply plugin: 'foundation.icon.javaee' + apply plugin: 'jacoco' java { sourceCompatibility = JavaVersion.VERSION_11 diff --git a/contracts/javascore/gradle.properties b/contracts/javascore/gradle.properties index 458ef894e..fb95e3946 100644 --- a/contracts/javascore/gradle.properties +++ b/contracts/javascore/gradle.properties @@ -7,4 +7,5 @@ iconsdkVersion=2.3.0 jupiterApiVersion=5.9.2 jupiterParamsVersion=5.9.2 jupiterEngineVersion=5.9.2 -javaeePluginVersion=0.8.2 \ No newline at end of file +javaeePluginVersion=0.8.2 +mockitoCoreVersion=4.5.1 diff --git a/contracts/javascore/ibc/build.gradle b/contracts/javascore/ibc/build.gradle index cd9be34e7..58d614606 100644 --- a/contracts/javascore/ibc/build.gradle +++ b/contracts/javascore/ibc/build.gradle @@ -6,15 +6,30 @@ dependencies { implementation project(':lib') implementation project(':score-util') + testImplementation("org.mockito:mockito-core:$mockitoCoreVersion") testImplementation("foundation.icon:javaee-unittest:$javaeeUnittestVersion") testAnnotationProcessor("foundation.icon:javaee-score-client:$scoreClientVersion") + testImplementation project(':test-lib') testImplementation("foundation.icon:javaee-score-client:$scoreClientVersion") testImplementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") testImplementation("foundation.icon:icon-sdk:$iconsdkVersion") + testImplementation("org.junit.jupiter:junit-jupiter-api:$jupiterApiVersion") + testImplementation("org.junit.jupiter:junit-jupiter-params:$jupiterParamsVersion") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$jupiterEngineVersion") } -test{ +test { useJUnitPlatform() + finalizedBy jacocoTestReport +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = false + csv.required = false + html.outputLocation = layout.buildDirectory.dir('jacocoHtml') + } } optimizedJar { diff --git a/contracts/javascore/ibc/src/main/java/ibc/ics02/client/IBCClient.java b/contracts/javascore/ibc/src/main/java/ibc/ics02/client/IBCClient.java index e0bf366c4..a6fdf1019 100644 --- a/contracts/javascore/ibc/src/main/java/ibc/ics02/client/IBCClient.java +++ b/contracts/javascore/ibc/src/main/java/ibc/ics02/client/IBCClient.java @@ -1,16 +1,19 @@ package ibc.ics02.client; +import ibc.icon.interfaces.IIBCClient; +import ibc.icon.interfaces.ILightClient; import ibc.icon.score.util.Logger; import ibc.icon.score.util.NullChecker; -import ibc.icon.structs.messages.MsgCreateClient; -import ibc.icon.structs.messages.MsgUpdateClient; -import ibc.ics24.host.IBCStore; +import ibc.icon.structs.messages.*; +import ibc.ics24.host.IBCCommitment; +import ibc.ics24.host.IBCHost; import score.Address; +import score.Context; import score.annotation.External; import java.math.BigInteger; -public class IBCClient extends IBCStore { +public class IBCClient extends IBCHost implements IIBCClient { Logger logger = new Logger("ibc-core"); @@ -22,35 +25,48 @@ public class IBCClient extends IBCStore { */ @External public void registerClient(String clientType, Address lightClient) { - NullChecker.requireNotNull(clientRegistry.get(clientType), "Already registered"); + Context.require(clientRegistry.get(clientType) == null, "Already registered."); clientRegistry.set(clientType, lightClient); } - @External - public void createClient(MsgCreateClient msg) { + public String createClient(MsgCreateClient msg) { String clientType = msg.clientType; - NullChecker.requireNotNull(clientType, "Client Type cannot be null"); - Address lightClientAddr = clientRegistry.get(clientType); - NullChecker.requireNotNull(clientType, "Register client before creation."); + NullChecker.requireNotNull(lightClientAddr, "Register client before creation."); String clientId = generateClientIdentifier(clientType); logger.println("Create Client: ", " clientId: ", clientId); + clientTypes.set(clientId, msg.clientType); clientImplementations.set(clientId, lightClientAddr); + ILightClient client = getClient(clientId); + CreateClientResponse response = client.createClient(clientId, msg.clientState, msg.consensusState); + Context.require(response.ok); - // + // update commitments + commitments.set(IBCCommitment.clientStateCommitmentKey(clientId), response.clientStateCommitment); + byte[] consensusKey = IBCCommitment.consensusStateCommitmentKey(clientId, + response.update.height.getRevisionNumber(), response.update.height.getRevisionHeight()); + commitments.set(consensusKey, response.update.consensusStateCommitment); + + return clientId; } - @External public void updateClient(MsgUpdateClient msg) { String clientId = msg.clientId; - NullChecker.requireNotNull(clientId, "ClientId cannot be null"); + ILightClient client = getClient(clientId); - Address lightClientAddr = clientImplementations.get(clientId); - NullChecker.requireNotNull(lightClientAddr, "Invalid client id"); + Context.require(commitments.get(IBCCommitment.clientStateCommitmentKey(clientId)) != null); + UpdateClientResponse response = client.updateClient(clientId, msg.clientMessage); + Context.require(response.ok); - // + // update commitments + commitments.set(IBCCommitment.clientStateCommitmentKey(clientId), response.clientStateCommitment); + for (ConsensusStateUpdate update : response.updates) { + byte[] consensusKey = IBCCommitment.consensusStateCommitmentKey(clientId, update.height.getRevisionNumber(), + update.height.getRevisionHeight()); + commitments.set(consensusKey, update.consensusStateCommitment); + } } private String generateClientIdentifier(String clientType) { diff --git a/contracts/javascore/ibc/src/main/java/ibc/ics03/connection/IBCConnection.java b/contracts/javascore/ibc/src/main/java/ibc/ics03/connection/IBCConnection.java index 8eddd0749..bce58e964 100644 --- a/contracts/javascore/ibc/src/main/java/ibc/ics03/connection/IBCConnection.java +++ b/contracts/javascore/ibc/src/main/java/ibc/ics03/connection/IBCConnection.java @@ -1,5 +1,262 @@ package ibc.ics03.connection; -public class IBCConnection { +import ibc.icon.interfaces.IIBCConnection; +import ibc.icon.interfaces.ILightClient; +import ibc.icon.score.util.Logger; +import ibc.icon.structs.messages.MsgConnectionOpenAck; +import ibc.icon.structs.messages.MsgConnectionOpenConfirm; +import ibc.icon.structs.messages.MsgConnectionOpenInit; +import ibc.icon.structs.messages.MsgConnectionOpenTry; +import ibc.icon.structs.proto.core.client.Height; +import ibc.icon.structs.proto.core.commitment.MerklePrefix; +import ibc.icon.structs.proto.core.connection.ConnectionEnd; +import ibc.icon.structs.proto.core.connection.Counterparty; +import ibc.icon.structs.proto.core.connection.Version; +import ibc.ics02.client.IBCClient; +import ibc.ics24.host.IBCCommitment; +import score.Context; + +import java.math.BigInteger; + +public class IBCConnection extends IBCClient implements IIBCConnection { + public static final String v1Identifier = "1"; + public static final String[] supportedV1Features = new String[]{"ORDER_ORDERED", "ORDER_UNORDERED"}; + public static final String commitmentPrefix = "ibc"; + + Logger logger = new Logger("ibc-core"); + + public String connectionOpenInit(MsgConnectionOpenInit msg) { + String connectionId = generateConnectionIdentifier(); + Context.require(connections.get(connectionId) == null, "connectionId already exists"); + ILightClient client = getClient(msg.clientId); + Context.require(client.getClientState(msg.clientId) != null, "Client state not found"); + + ConnectionEnd connection = new ConnectionEnd(); + connection.setClientId(msg.clientId); + connection.setVersions(getSupportedVersions()); + connection.setState(ConnectionEnd.State.STATE_INIT); + connection.setDelayPeriod(msg.delayPeriod); + connection.setCounterparty(msg.counterparty); + + updateConnectionCommitment(connectionId, connection); + connections.set(connectionId, connection); + + return connectionId; + } + + public String connectionOpenTry(MsgConnectionOpenTry msg) { + // TODO: investigate need to self client validation + Context.require(msg.counterpartyVersions.length > 0, "counterpartyVersions length must be greater than 0"); + + String connectionId = generateConnectionIdentifier(); + Context.require(connections.get(connectionId) == null, "connectionId already exists"); + + ConnectionEnd connection = new ConnectionEnd(); + connection.setClientId(msg.clientId); + connection.setVersions(getSupportedVersions()); + connection.setState(ConnectionEnd.State.STATE_TRYOPEN); + connection.setDelayPeriod(msg.delayPeriod); + connection.setCounterparty(msg.counterparty); + + MerklePrefix prefix = new MerklePrefix(); + prefix.setKeyPrefix(commitmentPrefix); + + Counterparty expectedCounterparty = new Counterparty(); + expectedCounterparty.setClientId(msg.clientId); + expectedCounterparty.setConnectionId(""); + expectedCounterparty.setPrefix(prefix); + + ConnectionEnd expectedConnection = new ConnectionEnd(); + expectedConnection.setClientId(msg.counterparty.getClientId()); + expectedConnection.setVersions(msg.counterpartyVersions); + expectedConnection.setState(ConnectionEnd.State.STATE_INIT); + expectedConnection.setDelayPeriod(msg.delayPeriod); + expectedConnection.setCounterparty(expectedCounterparty); + + verifyConnectionState(connection, msg.proofHeight, msg.proofInit, msg.counterparty.getConnectionId(), + expectedConnection); + + verifyClientState( + connection, + msg.proofHeight, + IBCCommitment.clientStatePath(connection.counterparty.getClientId()), + msg.proofClient, + msg.clientStateBytes); + // TODO we should also verify a consensus state + + updateConnectionCommitment(connectionId, connection); + connections.set(connectionId, connection); + + return connectionId; + } + + public void connectionOpenAck(MsgConnectionOpenAck msg) { + ConnectionEnd connection = connections.get(msg.connectionId); + Context.require(connection != null, "connection does not exist"); + ConnectionEnd.State state = connection.getState(); + // TODO should we allow the state to be TRY_OPEN? + Context.require(state.equals(ConnectionEnd.State.STATE_INIT) || state.equals(ConnectionEnd.State.STATE_TRYOPEN), + "connection state is not INIT or TRYOPEN"); + if (state.equals(ConnectionEnd.State.STATE_INIT)) { + Context.require(isSupportedVersion(msg.version), + "connection state is in INIT but the provided version is not supported"); + } else { + Context.require(connection.versions.length == 1 && connection.versions[0].equals(msg.version), + "connection state is in TRYOPEN but the provided version is not set in the previous connection " + + "versions"); + } + + // TODO: investigate need to self client validation + // require(validateSelfClient(msg.clientStateBytes), "failed to validate self + // client state"); + + MerklePrefix prefix = new MerklePrefix(); + prefix.setKeyPrefix(commitmentPrefix); + + Counterparty expectedCounterparty = new Counterparty(); + expectedCounterparty.setClientId(connection.getClientId()); + expectedCounterparty.setConnectionId(msg.connectionId); + expectedCounterparty.setPrefix(prefix); + + ConnectionEnd expectedConnection = new ConnectionEnd(); + expectedConnection.setClientId(connection.getClientId()); + expectedConnection.setVersions(new Version[]{msg.version}); + expectedConnection.setState(ConnectionEnd.State.STATE_TRYOPEN); + expectedConnection.setDelayPeriod(connection.delayPeriod); + expectedConnection.setCounterparty(expectedCounterparty); + + verifyConnectionState(connection, msg.proofHeight, msg.proofTry, msg.counterpartyConnectionID, + expectedConnection); + + verifyClientState( + connection, + msg.proofHeight, + IBCCommitment.clientStatePath(connection.counterparty.getClientId()), + msg.proofClient, + msg.clientStateBytes); + + // TODO: we should also verify a consensus state + + connection.setState(ConnectionEnd.State.STATE_OPEN); + connection.setVersions(expectedConnection.versions); + connection.counterparty.setConnectionId(msg.counterpartyConnectionID); + + updateConnectionCommitment(msg.connectionId, connection); + connections.set(msg.connectionId, connection); + + } + + public void connectionOpenConfirm(MsgConnectionOpenConfirm msg) { + ConnectionEnd connection = connections.get(msg.connectionId); + Context.require(connection != null, "connection does not exist"); + ConnectionEnd.State state = connection.getState(); + Context.require(state.equals(ConnectionEnd.State.STATE_TRYOPEN), "connection state is not TRYOPEN"); + + MerklePrefix prefix = new MerklePrefix(); + prefix.setKeyPrefix(commitmentPrefix); + + Counterparty expectedCounterparty = new Counterparty(); + expectedCounterparty.setClientId(connection.getClientId()); + expectedCounterparty.setConnectionId(msg.connectionId); + expectedCounterparty.setPrefix(prefix); + + ConnectionEnd expectedConnection = new ConnectionEnd(); + expectedConnection.setClientId(connection.getCounterparty().getClientId()); + expectedConnection.setVersions(connection.getVersions()); + expectedConnection.setState(ConnectionEnd.State.STATE_OPEN); + expectedConnection.setDelayPeriod(connection.delayPeriod); + expectedConnection.setCounterparty(expectedCounterparty); + + verifyConnectionState(connection, msg.proofHeight, msg.proofAck, connection.getCounterparty().getConnectionId(), + expectedConnection); + + connection.setState(ConnectionEnd.State.STATE_OPEN); + + updateConnectionCommitment(msg.connectionId, connection); + connections.set(msg.connectionId, connection); + } + + /* Verification functions */ + + private void verifyClientState(ConnectionEnd connection, Height height, byte[] path, byte[] proof, + byte[] clientStatebytes) { + ILightClient client = getClient(connection.getClientId()); + boolean ok = client.verifyMembership( + connection.getClientId(), + height, + BigInteger.ZERO, + BigInteger.ZERO, + proof, + connection.getCounterparty().getPrefix().getKeyPrefix(), + path, + clientStatebytes); + Context.require(ok, "failed to verify clientState"); + } + + private void verifyClientConsensusState(ConnectionEnd connection, Height height, Height consensusHeight, + byte[] proof, byte[] consensusStateBytes) { + byte[] consensusPath = IBCCommitment.consensusStatePath(connection.getCounterparty().getClientId(), + consensusHeight.getRevisionNumber(), + consensusHeight.getRevisionHeight()); + + ILightClient client = getClient(connection.getClientId()); + boolean ok = client.verifyMembership( + connection.getClientId(), + height, + BigInteger.ZERO, + BigInteger.ZERO, + proof, + connection.getCounterparty().getPrefix().getKeyPrefix(), + consensusPath, + consensusStateBytes); + Context.require(ok, "failed to verify consensus state"); + + } + + private void verifyConnectionState(ConnectionEnd connection, Height height, byte[] proof, String connectionId, + ConnectionEnd counterpartyConnection) { + ILightClient client = getClient(connection.getClientId()); + boolean ok = client.verifyMembership( + connection.getClientId(), + height, + BigInteger.ZERO, + BigInteger.ZERO, + proof, + connection.getCounterparty().getPrefix().getKeyPrefix(), + IBCCommitment.connectionPath(connectionId), + counterpartyConnection.toBytes()); + Context.require(ok, "failed to verify connection state"); + } + + /* Internal functions */ + + private String generateConnectionIdentifier() { + BigInteger currConnectionSequence = nextConnectionSequence.getOrDefault(BigInteger.ZERO); + String identifier = "connection-" + currConnectionSequence.toString(); + nextConnectionSequence.set(currConnectionSequence.add(BigInteger.ONE)); + + return identifier; + } + + /** + * {@code @dev} getSupportedVersions return the supported versions. + */ + private Version[] getSupportedVersions() { + Version version = new Version(); + version.setFeatures(supportedV1Features); + version.setIdentifier(v1Identifier); + + return new Version[]{version}; + } + + // TODO implement + private boolean isSupportedVersion(Version version) { + return true; + } + + private void updateConnectionCommitment(String connectionId, ConnectionEnd connection) { + commitments.set(IBCCommitment.connectionCommitmentKey(connectionId), + IBCCommitment.keccak256(connection.toBytes())); + } } diff --git a/contracts/javascore/ibc/src/main/java/ibc/ics04/channel/IBCChannel.java b/contracts/javascore/ibc/src/main/java/ibc/ics04/channel/IBCChannel.java deleted file mode 100644 index 6a4b6808d..000000000 --- a/contracts/javascore/ibc/src/main/java/ibc/ics04/channel/IBCChannel.java +++ /dev/null @@ -1,5 +0,0 @@ -package ibc.ics04.channel; - -public class IBCChannel { - -} diff --git a/contracts/javascore/ibc/src/main/java/ibc/ics04/channel/IBCChannelHandshake.java b/contracts/javascore/ibc/src/main/java/ibc/ics04/channel/IBCChannelHandshake.java new file mode 100644 index 000000000..9ed2a3563 --- /dev/null +++ b/contracts/javascore/ibc/src/main/java/ibc/ics04/channel/IBCChannelHandshake.java @@ -0,0 +1,264 @@ +package ibc.ics04.channel; + +import ibc.icon.interfaces.IIBCChannelHandshake; +import ibc.icon.interfaces.ILightClient; +import ibc.icon.structs.messages.*; +import ibc.icon.structs.proto.core.channel.Channel; +import ibc.icon.structs.proto.core.channel.Counterparty; +import ibc.icon.structs.proto.core.client.Height; +import ibc.icon.structs.proto.core.connection.ConnectionEnd; +import ibc.ics03.connection.IBCConnection; +import ibc.ics24.host.IBCCommitment; +import score.Context; + +import java.math.BigInteger; + +public class IBCChannelHandshake extends IBCConnection implements IIBCChannelHandshake { + + public String channelOpenInit(MsgChannelOpenInit msg) { + Context.require(msg.channel.connectionHops.length == 1, "connection_hops length must be 1"); + + ConnectionEnd connection = connections.get(msg.channel.connectionHops[0]); + Context.require(connection != null, "connection does not exist"); + Context.require( + connection.versions.length == 1, + "single version must be negotiated on connection before opening channel"); + + Context.require(msg.channel.getState().equals(Channel.State.STATE_INIT), "channel state must be STATE_INIT"); + + // TODO: verifySupportedFeature + // TODO: authenticates a port binding + + String channelId = generateChannelIdentifier(); + channels.at(msg.portId).set(channelId, msg.channel); + nextSequenceSends.at(msg.portId).set(channelId, BigInteger.ONE); + nextSequenceReceives.at(msg.portId).set(channelId, BigInteger.ONE); + nextSequenceAcknowledgements.at(msg.portId).set(channelId, BigInteger.ONE); + + updateChannelCommitment(msg.portId, channelId, msg.channel); + + return channelId; + } + + public String channelOpenTry(MsgChannelOpenTry msg) { + Context.require(msg.channel.getConnectionHops().length == 1, "connection_hops length must be 1"); + ConnectionEnd connection = connections.get(msg.channel.getConnectionHops()[0]); + Context.require(connection != null, "connection does not exist"); + Context.require( + connection.versions.length == 1, + "single version must be negotiated on connection before opening channel"); + Context.require(msg.channel.getState().equals(Channel.State.STATE_TRYOPEN), + "channel state must be STATE_TRYOPEN"); + + // TODO verifySupportedFeature + + // TODO authenticates a port binding + + Counterparty expectedCounterparty = new Counterparty(); + expectedCounterparty.setPortId(msg.portId); + expectedCounterparty.setChannelId(""); + + Channel expectedChannel = new Channel(); + expectedChannel.setState(Channel.State.STATE_INIT); + expectedChannel.setOrdering(msg.channel.getOrdering()); + expectedChannel.setCounterparty(expectedCounterparty); + expectedChannel.setConnectionHops(getCounterpartyHops(msg.channel.getConnectionHops()[0])); + expectedChannel.setVersion(msg.counterpartyVersion); + + verifyChannelState( + connection, + msg.proofHeight, + msg.proofInit, + msg.channel.getCounterparty().getPortId(), + msg.channel.getCounterparty().getChannelId(), + expectedChannel); + + String channelId = generateChannelIdentifier(); + channels.at(msg.portId).set(channelId, msg.channel); + nextSequenceSends.at(msg.portId).set(channelId, BigInteger.ONE); + nextSequenceReceives.at(msg.portId).set(channelId, BigInteger.ONE); + nextSequenceAcknowledgements.at(msg.portId).set(channelId, BigInteger.ONE); + + updateChannelCommitment(msg.portId, channelId, msg.channel); + + return channelId; + } + + public void channelOpenAck(MsgChannelOpenAck msg) { + Channel channel = channels.at(msg.portId).get(msg.channelId); + Context.require(channel != null, "channel does not exist"); + Context.require(channel.getConnectionHops().length == 1); + + Context.require( + channel.getState().equals(Channel.State.STATE_INIT) + || channel.getState().equals(Channel.State.STATE_TRYOPEN), + "invalid channel state"); + + // TODO authenticates a port binding + + ConnectionEnd connection = connections.get(channel.getConnectionHops()[0]); + Context.require(connection != null, "connection does not exist"); + Context.require(connection.getState().equals(ConnectionEnd.State.STATE_OPEN), "connection state is not OPEN"); + + Counterparty expectedCounterparty = new Counterparty(); + expectedCounterparty.setPortId(msg.portId); + expectedCounterparty.setChannelId(msg.channelId); + + Channel expectedChannel = new Channel(); + expectedChannel.setState(Channel.State.STATE_TRYOPEN); + expectedChannel.setOrdering(channel.getOrdering()); + expectedChannel.setCounterparty(expectedCounterparty); + expectedChannel.setConnectionHops(getCounterpartyHops(channel.getConnectionHops()[0])); + expectedChannel.setVersion(msg.counterpartyVersion); + + verifyChannelState( + connection, + msg.proofHeight, + msg.proofTry, + channel.getCounterparty().getPortId(), + msg.counterpartyChannelId, + expectedChannel); + + channel.setState(Channel.State.STATE_OPEN); + channel.setVersion(msg.counterpartyVersion); + channel.getCounterparty().setChannelId(msg.counterpartyChannelId); + + updateChannelCommitment(msg.portId, msg.channelId, channel); + channels.at(msg.portId).set(msg.channelId, channel); + } + + public void channelOpenConfirm(MsgChannelOpenConfirm msg) { + Channel channel = channels.at(msg.portId).get(msg.channelId); + Context.require(channel != null, "channel does not exist"); + Context.require(channel.getConnectionHops().length == 1); + Context.require(channel.getState().equals(Channel.State.STATE_TRYOPEN), "channel state is not TRYOPEN"); + + // TODO authenticates a port binding + + ConnectionEnd connection = connections.get(channel.getConnectionHops()[0]); + Context.require(connection != null, "connection does not exist"); + Context.require(connection.getState().equals(ConnectionEnd.State.STATE_OPEN), "connection state is not OPEN"); + + Counterparty expectedCounterparty = new Counterparty(); + expectedCounterparty.setPortId(msg.portId); + expectedCounterparty.setChannelId(msg.channelId); + + Channel expectedChannel = new Channel(); + expectedChannel.setState(Channel.State.STATE_OPEN); + expectedChannel.setOrdering(channel.getOrdering()); + expectedChannel.setCounterparty(expectedCounterparty); + expectedChannel.setConnectionHops(getCounterpartyHops(channel.getConnectionHops()[0])); + expectedChannel.setVersion(channel.getVersion()); + + verifyChannelState( + connection, + msg.proofHeight, + msg.proofAck, + channel.getCounterparty().getPortId(), + channel.getCounterparty().getChannelId(), + expectedChannel); + + channel.setState(Channel.State.STATE_OPEN); + + updateChannelCommitment(msg.portId, msg.channelId, channel); + channels.at(msg.portId).set(msg.channelId, channel); + } + + public void channelCloseInit(MsgChannelCloseInit msg) { + Channel channel = channels.at(msg.portId).get(msg.channelId); + Context.require(channel != null, "channel does not exist"); + Context.require(channel.getState() != Channel.State.STATE_CLOSED, "channel state is already CLOSED"); + + // TODO authenticates a port binding + + ConnectionEnd connection = connections.get(channel.getConnectionHops()[0]); + Context.require(connection != null, "connection does not exist"); + Context.require(connection.getState().equals(ConnectionEnd.State.STATE_OPEN), "connection state is not OPEN"); + + channel.setState(Channel.State.STATE_CLOSED); + + updateChannelCommitment(msg.portId, msg.channelId, channel); + channels.at(msg.portId).set(msg.channelId, channel); + } + + public void channelCloseConfirm(MsgChannelCloseConfirm msg) { + Channel channel = channels.at(msg.portId).get(msg.channelId); + Context.require(channel != null, "channel does not exist"); + Context.require(channel.getState() != Channel.State.STATE_CLOSED, "channel state is already CLOSED"); + Context.require(channel.getConnectionHops().length == 1); + + // TODO authenticates a port binding + + ConnectionEnd connection = connections.get(channel.getConnectionHops()[0]); + Context.require(connection != null, "connection does not exist"); + Context.require(connection.getState().equals(ConnectionEnd.State.STATE_OPEN), "connection state is not OPEN"); + + Counterparty expectedCounterparty = new Counterparty(); + expectedCounterparty.setPortId(msg.portId); + expectedCounterparty.setChannelId(msg.channelId); + + Channel expectedChannel = new Channel(); + expectedChannel.setState(Channel.State.STATE_CLOSED); + expectedChannel.setOrdering(channel.getOrdering()); + expectedChannel.setCounterparty(expectedCounterparty); + expectedChannel.setConnectionHops(getCounterpartyHops(channel.getConnectionHops()[0])); + expectedChannel.setVersion(channel.getVersion()); + + verifyChannelState( + connection, + msg.proofHeight, + msg.proofInit, + channel.getCounterparty().getPortId(), + channel.getCounterparty().getChannelId(), + expectedChannel); + + channel.setState(Channel.State.STATE_CLOSED); + + updateChannelCommitment(msg.portId, msg.channelId, channel); + channels.at(msg.portId).set(msg.channelId, channel); + } + + private void updateChannelCommitment(String portId, String channelId, Channel channel) { + commitments.set(IBCCommitment.channelCommitmentKey(portId, channelId), + IBCCommitment.keccak256(channel.toBytes())); + } + + /* Verification functions */ + + private void verifyChannelState( + ConnectionEnd connection, + Height height, + byte[] proof, + String portId, + String channelId, + Channel channel) { + ILightClient client = getClient(connection.getClientId()); + boolean ok = client.verifyMembership( + connection.getClientId(), + height, + BigInteger.ZERO, + BigInteger.ZERO, + proof, + connection.getCounterparty().getPrefix().getKeyPrefix(), + IBCCommitment.channelPath(portId, channelId), + channel.toBytes()); + Context.require(ok, "failed to verify channel state"); + + } + + /* Internal functions */ + + private String[] getCounterpartyHops(String connectionId) { + String hop = connections.get(connectionId).getCounterparty().getConnectionId(); + String[] hops = new String[]{hop}; + return hops; + } + + private String generateChannelIdentifier() { + BigInteger currChannelSequence = nextChannelSequence.getOrDefault(BigInteger.ZERO); + String identifier = "channel-" + currChannelSequence.toString(); + nextChannelSequence.set(currChannelSequence.add(BigInteger.ONE)); + + return identifier; + } +} diff --git a/contracts/javascore/ibc/src/main/java/ibc/ics04/channel/IBCPacket.java b/contracts/javascore/ibc/src/main/java/ibc/ics04/channel/IBCPacket.java new file mode 100644 index 000000000..412a62aff --- /dev/null +++ b/contracts/javascore/ibc/src/main/java/ibc/ics04/channel/IBCPacket.java @@ -0,0 +1,242 @@ +package ibc.ics04.channel; + +import ibc.icon.interfaces.IIBCPacket; +import ibc.icon.interfaces.ILightClient; +import ibc.icon.score.util.StringUtil; +import ibc.icon.structs.messages.MsgPacketAcknowledgement; +import ibc.icon.structs.messages.MsgPacketRecv; +import ibc.icon.structs.proto.core.channel.Channel; +import ibc.icon.structs.proto.core.channel.Packet; +import ibc.icon.structs.proto.core.client.Height; +import ibc.icon.structs.proto.core.connection.ConnectionEnd; +import ibc.ics24.host.IBCCommitment; +import score.Context; +import score.DictDB; + +import java.math.BigInteger; +import java.util.Arrays; + +// TODO verify packet commitments follow a correct format +public class IBCPacket extends IBCChannelHandshake implements IIBCPacket { + public void sendPacket(Packet packet) { + Channel channel = channels.at(packet.getSourcePort()).get(packet.getSourceChannel()); + Context.require(channel.getState() == Channel.State.STATE_OPEN, "channel state must be OPEN"); + Context.require( + packet.getDestinationPort().equals(channel.getCounterparty().getPortId()), + "packet destination port doesn't match the counterparty's port"); + Context.require( + packet.getDestinationChannel().equals(channel.getCounterparty().getChannelId()), + "packet destination channel doesn't match the counterparty's channel"); + + ConnectionEnd connection = connections.get(channel.getConnectionHops()[0]); + Context.require(connection != null, "connection does not exist"); + ILightClient client = getClient(connection.getClientId()); + Height latestHeight = client.getLatestHeight(connection.getClientId()); + + Context.require( + packet.getTimeoutHeight().isZero() || latestHeight.lt(packet.getTimeoutHeight()), + "receiving chain block height >= packet timeout height"); + BigInteger latestTimestamp = client.getTimestampAtHeight(connection.getClientId(), latestHeight); + Context.require(latestTimestamp != null, "consensusState not found"); + Context.require( + packet.getTimeoutTimestamp().equals(BigInteger.ZERO) + || latestTimestamp.compareTo(packet.getTimeoutTimestamp()) < 0, + "receiving chain block timestamp >= packet timeout timestamp"); + + DictDB nextSequenceSourcePort = nextSequenceSends.at(packet.getSourcePort()); + BigInteger nextSequenceSend = nextSequenceSourcePort.getOrDefault(packet.getSourceChannel(), BigInteger.ZERO); + Context.require( + packet.getSequence().equals(nextSequenceSend), + "packet sequence != next send sequence"); + + nextSequenceSourcePort.set(packet.getSourceChannel(), nextSequenceSend.add(BigInteger.ONE)); + + byte[] packetCommitmentKey = IBCCommitment.packetCommitmentKey(packet.getSourcePort(), + packet.getSourceChannel(), + packet.getSequence()); + + byte[] packetCommitment = IBCCommitment.keccak256(getPacketCommitment(packet)); + commitments.set(packetCommitmentKey, packetCommitment); + } + + public void recvPacket(MsgPacketRecv msg) { + Channel channel = channels.at(msg.packet.getSourcePort()).get(msg.packet.getSourceChannel()); + Context.require(channel.getState() == Channel.State.STATE_OPEN, "channel state must be OPEN"); + + // TODO + // Authenticate capability to ensure caller has authority to receive packet on + // this channel + + Context.require( + msg.packet.getDestinationPort().equals(channel.getCounterparty().getPortId()), + "packet destination port doesn't match the counterparty's port"); + Context.require( + msg.packet.getDestinationChannel().equals(channel.getCounterparty().getChannelId()), + "packet destination channel doesn't match the counterparty's channel"); + + ConnectionEnd connection = connections.get(channel.getConnectionHops()[0]); + Context.require(connection != null, "connection does not exist"); + Context.require(connection.getState().equals(ConnectionEnd.State.STATE_OPEN), "connection state is not OPEN"); + + Context.require( + msg.packet.getTimeoutHeight().getRevisionHeight().equals(BigInteger.ZERO) + || BigInteger.valueOf(Context.getBlockHeight()) + .compareTo(msg.packet.getTimeoutHeight().getRevisionHeight()) < 0, + "block height >= packet timeout height"); + Context.require( + msg.packet.getTimeoutTimestamp().equals(BigInteger.ZERO) + || BigInteger.valueOf(Context.getBlockTimestamp()) + .compareTo(msg.packet.getTimeoutTimestamp()) < 0, + "block timestamp >= packet timeout timestamp"); + + byte[] commitmentPath = IBCCommitment.packetCommitmentPath(msg.packet.getSourcePort(), + msg.packet.getSourceChannel(), msg.packet.getSequence()); + byte[] commitmentBytes = IBCCommitment.keccak256(getPacketCommitment(msg.packet)); + + verifyPacketCommitment( + connection, + msg.proofHeight, + msg.proof, + commitmentPath, + commitmentBytes); + + if (channel.getOrdering().equals(Channel.Order.ORDER_UNORDERED)) { + DictDB packetReceipt = packetReceipts.at(msg.packet.getDestinationPort()) + .at(msg.packet.getDestinationChannel()); + Context.require( + packetReceipt.get(msg.packet.getSequence()) == null, + "packet sequence already has been received"); + packetReceipt.set(msg.packet.getSequence(), BigInteger.ONE); + } else if (channel.getOrdering().equals(Channel.Order.ORDER_ORDERED)) { + DictDB nextSequenceDestinationPort = + nextSequenceReceives.at(msg.packet.getDestinationPort()); + BigInteger nextSequenceRecv = nextSequenceDestinationPort.getOrDefault(msg.packet.getDestinationChannel() + , BigInteger.ZERO); + Context.require( + nextSequenceRecv.equals(msg.packet.sequence), + "packet sequence != next receive sequence"); + nextSequenceDestinationPort.set(msg.packet.getDestinationChannel(), nextSequenceRecv.add(BigInteger.ONE)); + } else { + Context.revert("unknown ordering type"); + } + } + + public void writeAcknowledgement(String destinationPortId, String destinationChannel, BigInteger sequence, + byte[] acknowledgement) { + Context.require(acknowledgement.length > 0, "acknowledgement cannot be empty"); + + Channel channel = channels.at(destinationPortId).get(destinationChannel); + Context.require(channel.getState() == Channel.State.STATE_OPEN, "channel state must be OPEN"); + + byte[] ackCommitmentKey = IBCCommitment.packetAcknowledgementCommitmentKey(destinationPortId, + destinationChannel, sequence); + Context.require(commitments.get(ackCommitmentKey) == null, "acknowledgement for packet already exists"); + byte[] ackCommitment = IBCCommitment.keccak256(IBCCommitment.sha256(acknowledgement)); + commitments.set(ackCommitmentKey, ackCommitment); + } + + public void acknowledgePacket(MsgPacketAcknowledgement msg) { + Channel channel = channels.at(msg.packet.getSourcePort()).get(msg.packet.getSourceChannel()); + Context.require(channel.getState() == Channel.State.STATE_OPEN, "channel state must be OPEN"); + + Context.require( + msg.packet.getDestinationPort().equals(channel.getCounterparty().getPortId()), + "packet destination port doesn't match the counterparty's port"); + Context.require( + msg.packet.getDestinationChannel().equals(channel.getCounterparty().getChannelId()), + "packet destination channel doesn't match the counterparty's channel"); + + ConnectionEnd connection = connections.get(channel.getConnectionHops()[0]); + Context.require(connection != null, "connection does not exist"); + Context.require(connection.getState().equals(ConnectionEnd.State.STATE_OPEN), "connection state is not OPEN"); + + byte[] packetCommitmentKey = IBCCommitment.packetCommitmentKey(msg.packet.getSourcePort(), + msg.packet.getSourceChannel(), msg.packet.getSequence()); + byte[] packetCommitment = commitments.get(packetCommitmentKey); + Context.require(packetCommitment != null, "packet commitment not found"); + byte[] commitment = IBCCommitment.keccak256(getPacketCommitment(msg.packet)); + + Context.require(Arrays.equals(packetCommitment, commitment), "commitment byte[] are not equal"); + + byte[] packetAckPath = IBCCommitment.packetAcknowledgementCommitmentPath(msg.packet.destinationPort, + msg.packet.destinationChannel, msg.packet.sequence); + verifyPacketAcknowledgement( + connection, + msg.proofHeight, + msg.proof, + packetAckPath, + IBCCommitment.sha256(msg.acknowledgement)); + + if (channel.getOrdering().equals(Channel.Order.ORDER_ORDERED)) { + DictDB nextSequenceAckSourcePort = + nextSequenceAcknowledgements.at(msg.packet.getSourcePort()); + BigInteger nextSequenceAck = nextSequenceAckSourcePort.get(msg.packet.getSourceChannel()); + Context.require( + nextSequenceAck.equals(msg.packet.sequence), + "packet sequence != next ack sequence"); + nextSequenceAckSourcePort.set(msg.packet.getSourceChannel(), nextSequenceAck.add(BigInteger.ONE)); + } + + commitments.set(packetCommitmentKey, null); + } + + /* Verification functions */ + + private void verifyPacketCommitment( + ConnectionEnd connection, + Height height, + byte[] proof, + byte[] path, + byte[] commitmentBytes) { + ILightClient client = getClient(connection.getClientId()); + boolean ok = client.verifyMembership( + connection.getClientId(), + height, + connection.getDelayPeriod(), + calcBlockDelay(connection.getDelayPeriod()), + proof, + connection.getCounterparty().getPrefix().getKeyPrefix(), + path, + commitmentBytes); + Context.require(ok, "failed to verify packet commitment"); + } + + private void verifyPacketAcknowledgement( + ConnectionEnd connection, + Height height, + byte[] proof, + byte[] path, + byte[] acknowledgementCommitmentBytes) { + ILightClient client = getClient(connection.getClientId()); + boolean ok = client.verifyMembership( + connection.getClientId(), + height, + connection.getDelayPeriod(), + calcBlockDelay(connection.getDelayPeriod()), + proof, + connection.getCounterparty().getPrefix().getKeyPrefix(), + path, + acknowledgementCommitmentBytes); + Context.require(ok, "failed to verify packet acknowledgement commitment"); + } + + /* Internal functions */ + private BigInteger calcBlockDelay(BigInteger timeDelay) { + BigInteger blockDelay = BigInteger.ZERO; + BigInteger timePerBlock = expectedTimePerBlock.get(); + if (timePerBlock != null) { + blockDelay = timeDelay.add(timePerBlock).subtract(BigInteger.ONE).divide(timePerBlock); + } + + return blockDelay; + } + + private byte[] getPacketCommitment(Packet packet) { + return IBCCommitment.sha256( + StringUtil.encodePacked( + packet.getTimeoutTimestamp(), + packet.getTimeoutHeight().getRevisionNumber(), + packet.getTimeoutHeight().getRevisionHeight(), + packet.getData())); + } +} diff --git a/contracts/javascore/ibc/src/main/java/ibc/ics24/host/IBCCommitment.java b/contracts/javascore/ibc/src/main/java/ibc/ics24/host/IBCCommitment.java index ad3366da0..f8fddfe82 100644 --- a/contracts/javascore/ibc/src/main/java/ibc/ics24/host/IBCCommitment.java +++ b/contracts/javascore/ibc/src/main/java/ibc/ics24/host/IBCCommitment.java @@ -7,10 +7,20 @@ /** * // Commitment path generators that comply with - * path-space + * path-space */ public class IBCCommitment { private static final String KECCAK256 = "keccak-256"; + private static final String SHA256 = "sha-256"; + + public static byte[] keccak256(byte[] msg) { + return Context.hash(KECCAK256, msg); + } + + public static byte[] sha256(byte[] msg) { + return Context.hash(SHA256, msg); + } public static byte[] clientStatePath(String clientId) { return StringUtil.encodePacked("clients/", clientId, "/clientState"); @@ -78,4 +88,5 @@ public static byte[] packetReceiptCommitmentKey(String portId, String channelId, public static byte[] nextSequenceRecvCommitmentKey(String portId, String channelId) { return Context.hash(KECCAK256, nextSequenceRecvCommitmentPath(portId, channelId)); } + } diff --git a/contracts/javascore/ibc/src/main/java/ibc/ics24/host/IBCStore.java b/contracts/javascore/ibc/src/main/java/ibc/ics24/host/IBCStore.java index 07798a0d0..63c68a4a3 100644 --- a/contracts/javascore/ibc/src/main/java/ibc/ics24/host/IBCStore.java +++ b/contracts/javascore/ibc/src/main/java/ibc/ics24/host/IBCStore.java @@ -1,6 +1,12 @@ package ibc.ics24.host; +import ibc.icon.interfaces.ILightClient; +import ibc.icon.interfaces.ILightClientScoreInterface; +import ibc.icon.score.util.NullChecker; +import ibc.icon.structs.proto.core.channel.Channel; +import ibc.icon.structs.proto.core.connection.ConnectionEnd; import score.*; +import score.annotation.External; import java.math.BigInteger; @@ -33,15 +39,18 @@ public abstract class IBCStore { // clientID => clientImpl public final DictDB clientImplementations = Context.newDictDB(CLIENT_IMPLEMENTATIONS, Address.class); - // TODO: connections, channels - public final BranchDB> nextSequenceSends = - Context.newBranchDB(NEXT_SEQUENCE_SENDS, BigInteger.class); - public final BranchDB> nextSequenceReceives = - Context.newBranchDB(NEXT_SEQUENCE_RECEIVES, BigInteger.class); - public final BranchDB> nextSequenceAcknowledgements = - Context.newBranchDB(NEXT_SEQUENCE_ACKNOWLEDGEMENTS, BigInteger.class); - public final BranchDB>> packetReceipts = - Context.newBranchDB(PACKET_RECEIPTS, BigInteger.class); + + public final DictDB connections = Context.newDictDB(CONNECTIONS, ConnectionEnd.class); + public final BranchDB> channels = Context.newBranchDB(CHANNELS, Channel.class); + + public final BranchDB> nextSequenceSends = Context + .newBranchDB(NEXT_SEQUENCE_SENDS, BigInteger.class); + public final BranchDB> nextSequenceReceives = Context + .newBranchDB(NEXT_SEQUENCE_RECEIVES, BigInteger.class); + public final BranchDB> nextSequenceAcknowledgements = Context + .newBranchDB(NEXT_SEQUENCE_ACKNOWLEDGEMENTS, BigInteger.class); + public final BranchDB>> packetReceipts = Context + .newBranchDB(PACKET_RECEIPTS, BigInteger.class); public final BranchDB> capabilities = Context.newBranchDB(CAPABILITIES, Address.class); // Host Parameters @@ -53,4 +62,92 @@ public abstract class IBCStore { BigInteger.class); public final VarDB nextChannelSequence = Context.newVarDB(NEXT_CHANNEL_SEQUENCE, BigInteger.class); + @External(readonly = true) + public byte[] getCommitment(byte[] key) { + return commitments.get(key); + } + + @External(readonly = true) + public Address getClientRegistry(String type) { + return clientRegistry.get(type); + } + + @External(readonly = true) + public String getClientType(String clientId) { + return clientTypes.get(clientId); + } + + @External(readonly = true) + public Address getClientImplementation(String clientId) { + return clientImplementations.get(clientId); + } + + @External(readonly = true) + public ConnectionEnd getConnection(String connectionId) { + return connections.get(connectionId); + } + + @External(readonly = true) + public Channel getChannel(String portId, String channelId) { + return channels.at(portId).get(channelId); + } + + @External(readonly = true) + public BigInteger getNextSequenceSend(String portId, String channelId) { + return nextSequenceSends.at(portId).get(channelId); + } + + @External(readonly = true) + public BigInteger getNextSequenceReceive(String portId, String channelId) { + return nextSequenceReceives.at(portId).get(channelId); + } + + @External(readonly = true) + public BigInteger getNextSequenceAcknowledgement(String portId, String channelId) { + return nextSequenceAcknowledgements.at(portId).get(channelId); + } + + @External(readonly = true) + public BigInteger getPacketReceipt(String portId, String channelId, BigInteger sequence) { + return packetReceipts.at(portId).at(channelId).get(sequence); + } + + @External(readonly = true) + public String[] getCapability(byte[] name) { + ArrayDB
arrayDB = capabilities.at(name); + final int size = arrayDB.size(); + String[] capability = new String[size]; + for (int i = 0; i < size; i++) { + capability[i] = arrayDB.get(i).toString(); + } + + return capability; + } + + @External(readonly = true) + public BigInteger getExpectedTimePerBlock() { + return expectedTimePerBlock.get(); + } + + @External(readonly = true) + public BigInteger getNextClientSequence() { + return nextClientSequence.get(); + } + + @External(readonly = true) + public BigInteger getNextConnectionSequence() { + return nextConnectionSequence.get(); + } + + @External(readonly = true) + public BigInteger getNextChannelSequence() { + return nextChannelSequence.get(); + } + + public ILightClient getClient(String clientId) { + Address address = clientImplementations.get(clientId); + NullChecker.requireNotNull(address, "Client does not exist"); + return new ILightClientScoreInterface(address); + } + } diff --git a/contracts/javascore/ibc/src/test/java/ibc/ics02/client/ClientTest.java b/contracts/javascore/ibc/src/test/java/ibc/ics02/client/ClientTest.java new file mode 100644 index 000000000..7201c6848 --- /dev/null +++ b/contracts/javascore/ibc/src/test/java/ibc/ics02/client/ClientTest.java @@ -0,0 +1,169 @@ +package ibc.ics02.client; + +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 ibc.icon.interfaces.ILightClient; +import ibc.icon.interfaces.ILightClientScoreInterface; +import ibc.icon.structs.messages.*; +import ibc.icon.structs.proto.core.client.Height; +import ibc.icon.test.MockContract; +import ibc.ics24.host.IBCCommitment; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +import java.math.BigInteger; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ClientTest extends TestBase { + + private final ServiceManager sm = getServiceManager(); + private final Account owner = sm.createAccount(); + private Score client; + private MockContract lightClient; + + @BeforeEach + public void setup() throws Exception { + client = sm.deploy(owner, IBCClient.class); + lightClient = new MockContract<>(ILightClientScoreInterface.class, ILightClient.class, sm, owner); + } + + @Test + void registerClient_alreadyRegistered() { + // Arrange + String clientType = "clientType"; + client.invoke(owner, "registerClient", clientType, lightClient.getAddress()); + + // Act & Assert + String expectedErrorMessage = "Already registered"; + Executable registerWithSameType = () -> { + client.invoke(owner, "registerClient", clientType, + lightClient.getAddress()); + }; + AssertionError e = assertThrows(AssertionError.class, registerWithSameType); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void createClient_withoutType() { + // Arrange + MsgCreateClient msg = new MsgCreateClient(); + msg.clientType = "type"; + msg.consensusState = new byte[0]; + msg.clientState = new byte[0]; + + // Act & Assert + String expectedErrorMessage = "Register client before creation."; + Executable createWithoutRegisterExecutable = () -> client.invoke(owner, + "createClient", msg); + AssertionError e = assertThrows(AssertionError.class, + createWithoutRegisterExecutable); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void createClient() { + // Arrange + MsgCreateClient msg = new MsgCreateClient(); + msg.clientType = "type"; + msg.consensusState = new byte[2]; + msg.clientState = new byte[3]; + String expectedClientId = msg.clientType + "-0"; + + byte[] clientStateCommitment = new byte[4]; + byte[] consensusStateCommitment = new byte[5]; + Height consensusHeight = new Height(); + consensusHeight.setRevisionHeight(BigInteger.ONE); + consensusHeight.setRevisionNumber(BigInteger.TWO); + ConsensusStateUpdate update = new ConsensusStateUpdate(consensusStateCommitment, consensusHeight); + CreateClientResponse response = new CreateClientResponse(clientStateCommitment, update, true); + when(lightClient.mock.createClient(msg.clientType + "-" + BigInteger.ZERO, msg.clientState, + msg.consensusState)).thenReturn(response); + + // Act + client.invoke(owner, "registerClient", msg.clientType, lightClient.getAddress()); + client.invoke(owner, "createClient", msg); + + // Assert + byte[] storedClientStateCommitment = (byte[]) client.call("getCommitment", + IBCCommitment.clientStateCommitmentKey(expectedClientId)); + assertEquals(clientStateCommitment, storedClientStateCommitment); + + byte[] consensusKey = IBCCommitment.consensusStateCommitmentKey(expectedClientId, + consensusHeight.getRevisionNumber(), + consensusHeight.getRevisionHeight()); + byte[] storedConsensusStateCommitment = (byte[]) client.call("getCommitment", consensusKey); + assertArrayEquals(consensusStateCommitment, storedConsensusStateCommitment); + + assertEquals(BigInteger.ONE, client.call("getNextClientSequence")); + } + + @Test + public void updateClient_NonExistingClient() { + // Arrange + MsgUpdateClient updateMsg = new MsgUpdateClient(); + updateMsg.clientId = "nonType" + "-0"; + updateMsg.clientMessage = new byte[4]; + + // Act & Assert + String expectedErrorMessage = "Client does not exist"; + Executable updateWithoutCreate = () -> client.invoke(owner, "updateClient", updateMsg); + AssertionError e = assertThrows(AssertionError.class, + updateWithoutCreate); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + public void updateClient() { + // Arrange + createClient(); + MsgUpdateClient msg = new MsgUpdateClient(); + msg.clientId = "type-0"; + msg.clientMessage = new byte[4]; + + byte[] clientStateCommitment = new byte[4]; + byte[] consensusStateCommitment1 = new byte[5]; + byte[] consensusStateCommitment2 = new byte[6]; + + Height consensusHeight1 = new Height(); + consensusHeight1.setRevisionHeight(BigInteger.ONE); + consensusHeight1.setRevisionNumber(BigInteger.TWO); + Height consensusHeight2 = new Height(); + consensusHeight2.setRevisionHeight(BigInteger.valueOf(3)); + consensusHeight2.setRevisionNumber(BigInteger.valueOf(4)); + + ConsensusStateUpdate update1 = new ConsensusStateUpdate(consensusStateCommitment1, consensusHeight1); + ConsensusStateUpdate update2 = new ConsensusStateUpdate(consensusStateCommitment2, consensusHeight2); + ConsensusStateUpdate[] updates = new ConsensusStateUpdate[]{update1, update2}; + UpdateClientResponse response = new UpdateClientResponse(clientStateCommitment, updates, true); + + when(lightClient.mock.updateClient(msg.clientId, msg.clientMessage)).thenReturn(response); + + // Act + client.invoke(owner, "updateClient", msg); + + // Assert + verify(lightClient.mock).updateClient(msg.clientId, msg.clientMessage); + + byte[] storedClientStateCommitment = (byte[]) client.call("getCommitment", + IBCCommitment.clientStateCommitmentKey(msg.clientId)); + assertArrayEquals(clientStateCommitment, storedClientStateCommitment); + + byte[] consensusKey1 = IBCCommitment.consensusStateCommitmentKey(msg.clientId, + consensusHeight1.getRevisionNumber(), + consensusHeight1.getRevisionHeight()); + byte[] storedConsensusStateCommitment1 = (byte[]) client.call("getCommitment", consensusKey1); + assertArrayEquals(consensusStateCommitment1, storedConsensusStateCommitment1); + + byte[] consensusKey2 = IBCCommitment.consensusStateCommitmentKey(msg.clientId, + consensusHeight2.getRevisionNumber(), + consensusHeight2.getRevisionHeight()); + byte[] storedConsensusStateCommitment2 = (byte[]) client.call("getCommitment", consensusKey2); + assertArrayEquals(consensusStateCommitment2, storedConsensusStateCommitment2); + } +} diff --git a/contracts/javascore/ibc/src/test/java/ibc/ics03/connection/ConnectionTest.java b/contracts/javascore/ibc/src/test/java/ibc/ics03/connection/ConnectionTest.java new file mode 100644 index 000000000..6bc63fced --- /dev/null +++ b/contracts/javascore/ibc/src/test/java/ibc/ics03/connection/ConnectionTest.java @@ -0,0 +1,429 @@ +package ibc.ics03.connection; + +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 ibc.icon.interfaces.ILightClient; +import ibc.icon.interfaces.ILightClientScoreInterface; +import ibc.icon.structs.messages.MsgConnectionOpenAck; +import ibc.icon.structs.messages.MsgConnectionOpenConfirm; +import ibc.icon.structs.messages.MsgConnectionOpenInit; +import ibc.icon.structs.messages.MsgConnectionOpenTry; +import ibc.icon.structs.proto.core.client.Height; +import ibc.icon.structs.proto.core.commitment.MerklePrefix; +import ibc.icon.structs.proto.core.connection.ConnectionEnd; +import ibc.icon.structs.proto.core.connection.Counterparty; +import ibc.icon.structs.proto.core.connection.Version; +import ibc.icon.test.MockContract; +import ibc.ics24.host.IBCCommitment; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import score.Address; + +import java.math.BigInteger; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +public class ConnectionTest extends TestBase { + private final ServiceManager sm = getServiceManager(); + private final Account owner = sm.createAccount(); + private Score connection; + private MockContract lightClient; + + Height proofHeight = new Height(); + Height consensusHeight = new Height(); + + Counterparty counterparty = new Counterparty(); + MerklePrefix prefix = new MerklePrefix(); + Version version = new Version(); + + BigInteger delayPeriod = BigInteger.TEN; + String clientId = "type-0"; + + ConnectionEnd baseConnection = new ConnectionEnd(); + + public static class ConnectionMock extends IBCConnection { + public ConnectionMock() { + } + + public void setClient(String clientId, Address client) { + clientImplementations.set(clientId, client); + } + } + + @BeforeEach + public void setup() throws Exception { + connection = sm.deploy(owner, ConnectionMock.class); + + lightClient = new MockContract<>(ILightClientScoreInterface.class, ILightClient.class, sm, owner); + + proofHeight.revisionHeight = BigInteger.valueOf(5); + proofHeight.revisionNumber = BigInteger.valueOf(6); + proofHeight.revisionHeight = BigInteger.valueOf(7); + proofHeight.revisionNumber = BigInteger.valueOf(8); + + prefix.setKeyPrefix(IBCConnection.commitmentPrefix); + + counterparty.setClientId("counterpartyId"); + counterparty.setConnectionId("connectionId"); + counterparty.setPrefix(prefix); + + version.identifier = IBCConnection.v1Identifier; + version.features = IBCConnection.supportedV1Features; + + baseConnection.setClientId(clientId); + baseConnection.setVersions(new Version[]{version}); + baseConnection.setDelayPeriod(delayPeriod); + baseConnection.setCounterparty(counterparty); + + connection.invoke(owner, "setClient", clientId, lightClient.getAddress()); + } + + @Test + void connectionOpenInit_clientNotFound() { + // Arrange + MsgConnectionOpenInit msg = new MsgConnectionOpenInit(); + msg.clientId = "non existent"; + + // Act & Assert + String expectedErrorMessage = "Client does not exist"; + Executable openConnectionWithoutClient = () -> connection.invoke(owner, + "connectionOpenInit", msg); + AssertionError e = assertThrows(AssertionError.class, + openConnectionWithoutClient); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void connectionOpenInit_clientStateNotFound() { + // Arrange + MsgConnectionOpenInit msg = new MsgConnectionOpenInit(); + msg.clientId = clientId; + + // Act & Assert + String expectedErrorMessage = "Client state not found"; + Executable openConnectionWithoutState = () -> connection.invoke(owner, + "connectionOpenInit", msg); + AssertionError e = assertThrows(AssertionError.class, + openConnectionWithoutState); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void connectionOpenInit() { + // Arrange + MsgConnectionOpenInit msg = new MsgConnectionOpenInit(); + msg.clientId = clientId; + msg.counterparty = counterparty; + msg.delayPeriod = delayPeriod; + String expectedConnectionId = "connection-0"; + when(lightClient.mock.getClientState(msg.clientId)).thenReturn(new byte[0]); + + // Act + connection.invoke(owner, "connectionOpenInit", msg); + + // Assert + ConnectionEnd expectedConnection = baseConnection; + expectedConnection.setState(ConnectionEnd.State.STATE_INIT); + + byte[] storedCommitment = (byte[]) connection.call("getCommitment", + IBCCommitment.connectionCommitmentKey(expectedConnectionId)); + assertArrayEquals(IBCCommitment.keccak256(expectedConnection.toBytes()), storedCommitment); + assertEquals(BigInteger.ONE, connection.call("getNextConnectionSequence")); + } + + @Test + void connectionOpenTry_MissingVersion() { + // Arrange + MsgConnectionOpenTry msg = new MsgConnectionOpenTry(); + msg.counterpartyVersions = new Version[]{}; + + // Act & Assert + String expectedErrorMessage = "counterpartyVersions length must be greater than 0"; + Executable openConnectionWithoutVersion = () -> connection.invoke(owner, + "connectionOpenTry", msg); + AssertionError e = assertThrows(AssertionError.class, + openConnectionWithoutVersion); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void connectionOpenTry_failedConnectionStateVerification() { + // Arrange + MsgConnectionOpenTry msg = new MsgConnectionOpenTry(); + msg.counterpartyVersions = new Version[]{}; + + // Act & Assert + String expectedErrorMessage = "counterpartyVersions length must be greater than 0"; + Executable openConnectionWithoutVersion = () -> connection.invoke(owner, + "connectionOpenTry", msg); + AssertionError e = assertThrows(AssertionError.class, + openConnectionWithoutVersion); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void connectionOpenTry_invalidStates() { + // Arrange + MsgConnectionOpenTry msg = new MsgConnectionOpenTry(); + msg.clientId = clientId; + msg.counterparty = counterparty; + msg.delayPeriod = delayPeriod; + msg.clientStateBytes = new byte[1]; + msg.counterpartyVersions = new Version[]{version}; + msg.proofInit = new byte[2]; + msg.proofClient = new byte[3]; + msg.proofConsensus = new byte[4]; + msg.proofHeight = proofHeight; + msg.consensusHeight = consensusHeight; + + Counterparty expectedCounterparty = new Counterparty(); + expectedCounterparty.setClientId(msg.clientId); + expectedCounterparty.setConnectionId(""); + expectedCounterparty.setPrefix(prefix); + + ConnectionEnd counterpartyConnection = new ConnectionEnd(); + counterpartyConnection.setClientId(counterparty.getClientId()); + counterpartyConnection.setVersions(new Version[]{version}); + counterpartyConnection.setState(ConnectionEnd.State.STATE_INIT); + counterpartyConnection.setDelayPeriod(msg.delayPeriod); + counterpartyConnection.setCounterparty(expectedCounterparty); + + // verifyConnectionState + byte[] connectionPath = IBCCommitment.connectionPath(msg.counterparty.getConnectionId()); + when(lightClient.mock.verifyMembership(msg.clientId, + msg.proofHeight, BigInteger.ZERO, + BigInteger.ZERO, msg.proofInit, prefix.getKeyPrefix(), connectionPath, + counterpartyConnection.toBytes())) + .thenReturn(false).thenReturn(true); + + // verifyClientState + byte[] clientStatePath = IBCCommitment.clientStatePath(msg.counterparty.getClientId()); + when(lightClient.mock.verifyMembership(msg.clientId, msg.proofHeight, BigInteger.ZERO, + BigInteger.ZERO, msg.proofClient, prefix.getKeyPrefix(), clientStatePath, + msg.clientStateBytes)) + .thenReturn(false); + + // Act & Assert + String expectedErrorMessage = "failed to verify connection state"; + Executable clientVerificationFailed = () -> connection.invoke(owner, + "connectionOpenTry", msg); + AssertionError e = assertThrows(AssertionError.class, + clientVerificationFailed); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + + expectedErrorMessage = "failed to verify clientState"; + Executable stateVerificationFailed = () -> connection.invoke(owner, + "connectionOpenTry", msg); + e = assertThrows(AssertionError.class, + stateVerificationFailed); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + + } + + @Test + void connectionOpenTry() { + // Arrange + MsgConnectionOpenTry msg = new MsgConnectionOpenTry(); + msg.clientId = clientId; + msg.counterparty = counterparty; + msg.delayPeriod = delayPeriod; + msg.clientStateBytes = new byte[1]; + msg.counterpartyVersions = new Version[]{version}; + msg.proofInit = new byte[2]; + msg.proofClient = new byte[3]; + msg.proofConsensus = new byte[4]; + msg.proofHeight = proofHeight; + msg.consensusHeight = consensusHeight; + + Counterparty expectedCounterparty = new Counterparty(); + expectedCounterparty.setClientId(msg.clientId); + expectedCounterparty.setConnectionId(""); + expectedCounterparty.setPrefix(prefix); + + ConnectionEnd counterpartyConnection = new ConnectionEnd(); + counterpartyConnection.setClientId(counterparty.getClientId()); + counterpartyConnection.setVersions(new Version[]{version}); + counterpartyConnection.setState(ConnectionEnd.State.STATE_INIT); + counterpartyConnection.setDelayPeriod(msg.delayPeriod); + counterpartyConnection.setCounterparty(expectedCounterparty); + + String expectedConnectionId = "connection-0"; + + // verifyConnectionState + byte[] connectionPath = IBCCommitment.connectionPath(msg.counterparty.getConnectionId()); + when(lightClient.mock.verifyMembership(msg.clientId, + msg.proofHeight, BigInteger.ZERO, + BigInteger.ZERO, msg.proofInit, prefix.getKeyPrefix(), connectionPath, + counterpartyConnection.toBytes())).thenReturn(true); + + // verifyClientState + byte[] clientStatePath = IBCCommitment.clientStatePath(msg.counterparty.getClientId()); + when(lightClient.mock.verifyMembership(msg.clientId, msg.proofHeight, BigInteger.ZERO, + BigInteger.ZERO, msg.proofClient, prefix.getKeyPrefix(), clientStatePath, + msg.clientStateBytes)) + .thenReturn(true); + + // Act + connection.invoke(owner, "connectionOpenTry", msg); + + // Assert + ConnectionEnd expectedConnection = baseConnection; + expectedConnection.setState(ConnectionEnd.State.STATE_TRYOPEN); + + byte[] storedCommitment = (byte[]) connection.call("getCommitment", + IBCCommitment.connectionCommitmentKey(expectedConnectionId)); + assertArrayEquals(IBCCommitment.keccak256(expectedConnection.toBytes()), storedCommitment); + assertEquals(BigInteger.ONE, connection.call("getNextConnectionSequence")); + } + + @Test + void connectionOpenAck_alreadyOpen() { + // Arrange + connectionOpenConfirm(); + MsgConnectionOpenAck msg = new MsgConnectionOpenAck(); + msg.connectionId = "connection-0"; + + // Act & Assert + String expectedErrorMessage = "connection state is not INIT or TRYOPEN"; + Executable clientVerificationFailed = () -> connection.invoke(owner, + "connectionOpenAck", msg); + AssertionError e = assertThrows(AssertionError.class, + clientVerificationFailed); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void connectionOpenAck_wrongVersion() { + // Arrange + connectionOpenTry(); + MsgConnectionOpenAck msg = new MsgConnectionOpenAck(); + msg.connectionId = "connection-0"; + Version wrongVersion = new Version(); + wrongVersion.identifier = "OtherVersion"; + wrongVersion.features = new String[]{"some features"}; + msg.version = wrongVersion; + + // Act & Assert + String expectedErrorMessage = "connection state is in TRYOPEN but the provided version is not set in the " + + "previous connection versions"; + Executable clientVerificationFailed = () -> connection.invoke(owner, + "connectionOpenAck", msg); + AssertionError e = assertThrows(AssertionError.class, + clientVerificationFailed); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void connectionOpenAck() { + // Arrange + connectionOpenInit(); + MsgConnectionOpenAck msg = new MsgConnectionOpenAck(); + msg.connectionId = "connection-0"; + msg.clientStateBytes = new byte[1]; + msg.version = version; + msg.counterpartyConnectionID = counterparty.clientId; + msg.proofTry = new byte[2]; + msg.proofClient = new byte[3]; + msg.proofConsensus = new byte[4]; + msg.proofHeight = proofHeight; + msg.consensusHeight = consensusHeight; + + Counterparty expectedCounterparty = new Counterparty(); + expectedCounterparty.setClientId(clientId); + expectedCounterparty.setConnectionId(msg.connectionId); + expectedCounterparty.setPrefix(prefix); + + ConnectionEnd counterpartyConnection = new ConnectionEnd(); + counterpartyConnection.setClientId(clientId); + counterpartyConnection.setVersions(new Version[]{version}); + counterpartyConnection.setState(ConnectionEnd.State.STATE_TRYOPEN); + counterpartyConnection.setDelayPeriod(delayPeriod); + counterpartyConnection.setCounterparty(expectedCounterparty); + + // verifyConnectionState + byte[] connectionPath = IBCCommitment.connectionPath(msg.counterpartyConnectionID); + when(lightClient.mock.verifyMembership(clientId, msg.proofHeight, BigInteger.ZERO, BigInteger.ZERO, + msg.proofTry, prefix.getKeyPrefix(), connectionPath, counterpartyConnection.toBytes())) + .thenReturn(true); + + // verifyClientState + byte[] clientStatePath = IBCCommitment.clientStatePath(counterparty.clientId); + when(lightClient.mock.verifyMembership(clientId, msg.proofHeight, BigInteger.ZERO, BigInteger.ZERO, + msg.proofClient, prefix.getKeyPrefix(), clientStatePath, msg.clientStateBytes)).thenReturn(true); + + // Act + connection.invoke(owner, "connectionOpenAck", msg); + + // Assert + ConnectionEnd expectedConnection = baseConnection; + expectedConnection.setState(ConnectionEnd.State.STATE_OPEN); + expectedConnection.setVersions(counterpartyConnection.versions); + expectedConnection.counterparty.setConnectionId(msg.counterpartyConnectionID); + byte[] storedCommitment = (byte[]) connection.call("getCommitment", + IBCCommitment.connectionCommitmentKey(msg.connectionId)); + assertArrayEquals(IBCCommitment.keccak256(expectedConnection.toBytes()), storedCommitment); + assertEquals(BigInteger.ONE, connection.call("getNextConnectionSequence")); + } + + @Test + void connectionOpenConfirm_NotInTryOpen() { + // Arrange + connectionOpenInit(); + MsgConnectionOpenConfirm msg = new MsgConnectionOpenConfirm(); + msg.connectionId = "connection-0"; + msg.proofAck = new byte[1]; + msg.proofHeight = proofHeight; + + // Act & Assert + String expectedErrorMessage = "connection state is not TRYOPEN"; + Executable clientVerificationFailed = () -> connection.invoke(owner, + "connectionOpenConfirm", msg); + AssertionError e = assertThrows(AssertionError.class, + clientVerificationFailed); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void connectionOpenConfirm() { + // Arrange + connectionOpenTry(); + MsgConnectionOpenConfirm msg = new MsgConnectionOpenConfirm(); + msg.connectionId = "connection-0"; + msg.proofAck = new byte[1]; + msg.proofHeight = proofHeight; + + Counterparty expectedCounterparty = new Counterparty(); + expectedCounterparty.setClientId(clientId); + expectedCounterparty.setConnectionId(msg.connectionId); + expectedCounterparty.setPrefix(prefix); + + ConnectionEnd counterpartyConnection = new ConnectionEnd(); + counterpartyConnection.setClientId(counterparty.getClientId()); + counterpartyConnection.setVersions(new Version[]{version}); + counterpartyConnection.setState(ConnectionEnd.State.STATE_OPEN); + counterpartyConnection.setDelayPeriod(delayPeriod); + counterpartyConnection.setCounterparty(expectedCounterparty); + + // verifyConnectionState + byte[] connectionPath = IBCCommitment.connectionPath(counterparty.connectionId); + when(lightClient.mock.verifyMembership(clientId, msg.proofHeight, BigInteger.ZERO, BigInteger.ZERO, + msg.proofAck, prefix.getKeyPrefix(), connectionPath, counterpartyConnection.toBytes())) + .thenReturn(true); + + // Act + connection.invoke(owner, "connectionOpenConfirm", msg); + + // Assert + ConnectionEnd expectedConnection = baseConnection; + expectedConnection.setState(ConnectionEnd.State.STATE_OPEN); + byte[] storedCommitment = (byte[]) connection.call("getCommitment", + IBCCommitment.connectionCommitmentKey(msg.connectionId)); + assertArrayEquals(IBCCommitment.keccak256(expectedConnection.toBytes()), storedCommitment); + assertEquals(BigInteger.ONE, connection.call("getNextConnectionSequence")); + + } +} diff --git a/contracts/javascore/ibc/src/test/java/ibc/ics04/channel/ChannelHandshakeTest.java b/contracts/javascore/ibc/src/test/java/ibc/ics04/channel/ChannelHandshakeTest.java new file mode 100644 index 000000000..76de479be --- /dev/null +++ b/contracts/javascore/ibc/src/test/java/ibc/ics04/channel/ChannelHandshakeTest.java @@ -0,0 +1,471 @@ +package ibc.ics04.channel; + +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 ibc.icon.interfaces.ILightClient; +import ibc.icon.interfaces.ILightClientScoreInterface; +import ibc.icon.structs.messages.*; +import ibc.icon.structs.proto.core.channel.Channel; +import ibc.icon.structs.proto.core.channel.Counterparty; +import ibc.icon.structs.proto.core.client.Height; +import ibc.icon.structs.proto.core.commitment.MerklePrefix; +import ibc.icon.structs.proto.core.connection.ConnectionEnd; +import ibc.icon.structs.proto.core.connection.Version; +import ibc.icon.test.MockContract; +import ibc.ics03.connection.IBCConnection; +import ibc.ics24.host.IBCCommitment; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import score.Address; + +import java.math.BigInteger; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +public class ChannelHandshakeTest extends TestBase { + + private final ServiceManager sm = getServiceManager(); + private final Account owner = sm.createAccount(); + private Score channel; + private MockContract lightClient; + + Height proofHeight = new Height(); + String clientId = "clientId"; + String connectionId = "connectionId"; + ConnectionEnd baseConnection = new ConnectionEnd(); + Channel baseChannel = new Channel(); + MerklePrefix prefix = new MerklePrefix(); + Version version = new Version(); + ibc.icon.structs.proto.core.connection.Counterparty connectionCounterparty = + new ibc.icon.structs.proto.core.connection.Counterparty(); + Counterparty baseCounterparty = new Counterparty(); + String portId = "portId"; + String channelId = "channel-0"; + String channelVersion = IBCConnection.v1Identifier; + + public static class ChannelHandshakeMock extends IBCChannelHandshake { + public ChannelHandshakeMock() { + } + + public void setConnectionEnd(String connectionId, ConnectionEnd connectionEnd) { + connections.set(connectionId, connectionEnd); + } + + public void setClient(String clientId, Address client) { + clientImplementations.set(clientId, client); + } + } + + @BeforeEach + public void setup() throws Exception { + channel = sm.deploy(owner, ChannelHandshakeMock.class); + + lightClient = new MockContract<>(ILightClientScoreInterface.class, ILightClient.class, sm, owner); + + prefix.setKeyPrefix(IBCConnection.commitmentPrefix); + proofHeight.revisionHeight = BigInteger.valueOf(5); + proofHeight.revisionNumber = BigInteger.valueOf(6); + + connectionCounterparty.setClientId(clientId); + connectionCounterparty.setConnectionId(""); + connectionCounterparty.setPrefix(prefix); + version.identifier = IBCConnection.v1Identifier; + version.features = IBCConnection.supportedV1Features; + + baseConnection.setClientId(clientId); + baseConnection.setState(ConnectionEnd.State.STATE_OPEN); + baseConnection.setCounterparty(connectionCounterparty); + baseConnection.setDelayPeriod(BigInteger.ONE); + baseConnection.setVersions(new Version[]{version}); + + baseCounterparty.setPortId(portId); + baseCounterparty.setChannelId(channelId); + + baseChannel.setState(Channel.State.STATE_INIT); + baseChannel.setOrdering(Channel.Order.ORDER_ORDERED); + baseChannel.setCounterparty(baseCounterparty); + baseChannel.setConnectionHops(new String[]{connectionId}); + baseChannel.setVersion("v1"); + channel.invoke(owner, "setClient", clientId, lightClient.getAddress()); + } + + @Test + void channelOpenInit_multipleHops() { + // Arrange + addConnection(connectionId, baseConnection); + baseChannel.setConnectionHops(new String[]{connectionId, "otherId"}); + + MsgChannelOpenInit msg = new MsgChannelOpenInit(); + msg.portId = portId; + msg.channel = baseChannel; + + // Act & Assert + String expectedErrorMessage = "connection_hops length must be 1"; + Executable multiHopChannel = () -> channel.invoke(owner, + "channelOpenInit", msg); + AssertionError e = assertThrows(AssertionError.class, + multiHopChannel); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void channelOpenInit_noConnection() { + // Arrange + MsgChannelOpenInit msg = new MsgChannelOpenInit(); + msg.portId = portId; + msg.channel = baseChannel; + + // Act & Assert + String expectedErrorMessage = "connection does not exist"; + Executable withoutConnection = () -> channel.invoke(owner, + "channelOpenInit", msg); + AssertionError e = assertThrows(AssertionError.class, + withoutConnection); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void channelOpenInit_inconsistentVersion() { + // Arrange + baseConnection.setVersions(new Version[]{version, version}); + addConnection(connectionId, baseConnection); + MsgChannelOpenInit msg = new MsgChannelOpenInit(); + msg.portId = portId; + msg.channel = baseChannel; + + // Act & Assert + String expectedErrorMessage = "single version must be negotiated on connection before opening channel"; + Executable withoutNegotiatedVersion = () -> channel.invoke(owner, + "channelOpenInit", msg); + AssertionError e = assertThrows(AssertionError.class, + withoutNegotiatedVersion); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void channelOpenInit_wrongState() { + // Arrange + addConnection(connectionId, baseConnection); + baseChannel.setState(Channel.State.STATE_OPEN); + MsgChannelOpenInit msg = new MsgChannelOpenInit(); + msg.portId = portId; + msg.channel = baseChannel; + + // Act & Assert + String expectedErrorMessage = "channel state must be STATE_INIT"; + Executable withoutNegotiatedVersion = () -> channel.invoke(owner, + "channelOpenInit", msg); + AssertionError e = assertThrows(AssertionError.class, + withoutNegotiatedVersion); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void channelOpenInit() { + // Arrange + addConnection(connectionId, baseConnection); + + MsgChannelOpenInit msg = new MsgChannelOpenInit(); + msg.portId = portId; + msg.channel = baseChannel; + + // Act + channel.invoke(owner, "channelOpenInit", msg); + + // Assert + byte[] key = IBCCommitment.channelCommitmentKey(portId, channelId); + byte[] storedCommitment = (byte[]) channel.call("getCommitment", key); + + assertArrayEquals(IBCCommitment.keccak256(msg.channel.toBytes()), storedCommitment); + assertEquals(BigInteger.ONE, channel.call("getNextChannelSequence")); + assertEquals(BigInteger.ONE, channel.call("getNextSequenceReceive", portId, channelId)); + assertEquals(BigInteger.ONE, channel.call("getNextSequenceSend", portId, channelId)); + assertEquals(BigInteger.ONE, channel.call("getNextSequenceAcknowledgement", portId, channelId)); + } + + @Test + void channelOpenTry_multipleHops() { + // Arrange + baseChannel.setConnectionHops(new String[]{connectionId, "otherId"}); + + MsgChannelOpenTry msg = new MsgChannelOpenTry(); + msg.channel = baseChannel; + + // Act & Assert + String expectedErrorMessage = "connection_hops length must be 1"; + Executable multiHop = () -> channel.invoke(owner, "channelOpenTry", msg); + AssertionError e = assertThrows(AssertionError.class, + multiHop); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void channelOpenTry_noConnection() { + // Arrange + MsgChannelOpenTry msg = new MsgChannelOpenTry(); + msg.channel = baseChannel; + + // Act & Assert + String expectedErrorMessage = "connection does not exist"; + Executable noConnection = () -> channel.invoke(owner, "channelOpenTry", msg); + AssertionError e = assertThrows(AssertionError.class, + noConnection); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void channelOpenTry_inconsistentVersion() { + // Arrange + baseConnection.setVersions(new Version[]{version, version}); + addConnection(connectionId, baseConnection); + + MsgChannelOpenTry msg = new MsgChannelOpenTry(); + msg.channel = baseChannel; + + // Act & Assert + String expectedErrorMessage = "single version must be negotiated on connection before opening channel"; + Executable inconsistentVersion = () -> channel.invoke(owner, "channelOpenTry", msg); + AssertionError e = assertThrows(AssertionError.class, + inconsistentVersion); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void channelOpenTry_wrongState() { + // Arrange + addConnection(connectionId, baseConnection); + + baseChannel.setState(Channel.State.STATE_INIT); + MsgChannelOpenTry msg = new MsgChannelOpenTry(); + msg.channel = baseChannel; + + // Act & Assert + String expectedErrorMessage = "channel state must be STATE_TRYOPEN"; + Executable wrongState = () -> channel.invoke(owner, "channelOpenTry", msg); + AssertionError e = assertThrows(AssertionError.class, + wrongState); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void channelOpenTry_failedVerification() { + // Arrange + addConnection(connectionId, baseConnection); + baseChannel.setState(Channel.State.STATE_TRYOPEN); + + MsgChannelOpenTry msg = new MsgChannelOpenTry(); + msg.portId = portId; + msg.channel = baseChannel; + msg.counterpartyVersion = channelVersion; + msg.proofHeight = proofHeight; + msg.proofInit = new byte[1]; + + Counterparty expectedCounterparty = new Counterparty(); + expectedCounterparty.setPortId(msg.portId); + expectedCounterparty.setChannelId(""); + + Channel expectedChannel = new Channel(); + expectedChannel.setState(Channel.State.STATE_INIT); + expectedChannel.setOrdering(msg.channel.getOrdering()); + expectedChannel.setCounterparty(expectedCounterparty); + expectedChannel.setConnectionHops(new String[]{baseConnection.getCounterparty().getConnectionId()}); + expectedChannel.setVersion(msg.counterpartyVersion); + + when(lightClient.mock.verifyMembership(clientId, msg.proofHeight, BigInteger.ZERO, BigInteger.ZERO, + msg.proofInit, prefix.getKeyPrefix(), IBCCommitment.channelPath(portId, channelId), + expectedChannel.toBytes())).thenReturn(false); + + // Act & Assert + String expectedErrorMessage = "failed to verify channel state"; + Executable wrongState = () -> channel.invoke(owner, "channelOpenTry", msg); + AssertionError e = assertThrows(AssertionError.class, + wrongState); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void channelOpenTry() { + // Arrange + addConnection(connectionId, baseConnection); + baseChannel.setState(Channel.State.STATE_TRYOPEN); + + MsgChannelOpenTry msg = new MsgChannelOpenTry(); + msg.portId = portId; + msg.channel = baseChannel; + msg.counterpartyVersion = channelVersion; + msg.proofHeight = proofHeight; + msg.proofInit = new byte[1]; + + Counterparty expectedCounterparty = new Counterparty(); + expectedCounterparty.setPortId(msg.portId); + expectedCounterparty.setChannelId(""); + + Channel expectedChannel = new Channel(); + expectedChannel.setState(Channel.State.STATE_INIT); + expectedChannel.setOrdering(msg.channel.getOrdering()); + expectedChannel.setCounterparty(expectedCounterparty); + expectedChannel.setConnectionHops(new String[]{baseConnection.getCounterparty().getConnectionId()}); + expectedChannel.setVersion(msg.counterpartyVersion); + + when(lightClient.mock.verifyMembership(clientId, msg.proofHeight, BigInteger.ZERO, BigInteger.ZERO, + msg.proofInit, prefix.getKeyPrefix(), IBCCommitment.channelPath(portId, channelId), + expectedChannel.toBytes())).thenReturn(true); + // Act + channel.invoke(owner, "channelOpenTry", msg); + + // Assert + byte[] key = IBCCommitment.channelCommitmentKey(portId, channelId); + byte[] storedCommitment = (byte[]) channel.call("getCommitment", key); + + assertArrayEquals(IBCCommitment.keccak256(msg.channel.toBytes()), storedCommitment); + assertEquals(BigInteger.ONE, channel.call("getNextChannelSequence")); + assertEquals(BigInteger.ONE, channel.call("getNextSequenceReceive", portId, channelId)); + assertEquals(BigInteger.ONE, channel.call("getNextSequenceSend", portId, channelId)); + assertEquals(BigInteger.ONE, channel.call("getNextSequenceAcknowledgement", portId, channelId)); + } + + @Test + void channelOpenAck() { + channelOpenInit(); + MsgChannelOpenAck msg = new MsgChannelOpenAck(); + msg.portId = portId; + msg.channelId = channelId; + msg.counterpartyVersion = "v1"; + msg.counterpartyChannelId = channelId; + msg.proofTry = new byte[0]; + msg.proofHeight = proofHeight; + + Counterparty expectedCounterparty = new Counterparty(); + expectedCounterparty.setPortId(msg.portId); + expectedCounterparty.setChannelId(msg.channelId); + + Channel counterpartyChannel = new Channel(); + counterpartyChannel.setState(Channel.State.STATE_TRYOPEN); + counterpartyChannel.setOrdering(baseChannel.getOrdering()); + counterpartyChannel.setCounterparty(expectedCounterparty); + counterpartyChannel.setConnectionHops(new String[]{baseConnection.getCounterparty().getConnectionId()}); + counterpartyChannel.setVersion(msg.counterpartyVersion); + + when(lightClient.mock.verifyMembership(clientId, msg.proofHeight, BigInteger.ZERO, BigInteger.ZERO, + msg.proofTry, prefix.getKeyPrefix(), IBCCommitment.channelPath(portId, channelId), + counterpartyChannel.toBytes())).thenReturn(true); + + channel.invoke(owner, "channelOpenAck", msg); + + // Assert + byte[] key = IBCCommitment.channelCommitmentKey(portId, channelId); + byte[] storedCommitment = (byte[]) channel.call("getCommitment", key); + + Channel expectedChannel = baseChannel; + expectedChannel.setState(Channel.State.STATE_OPEN); + expectedChannel.setVersion(msg.counterpartyVersion); + expectedChannel.getCounterparty().setChannelId(msg.counterpartyChannelId); + assertArrayEquals(IBCCommitment.keccak256(expectedChannel.toBytes()), storedCommitment); + } + + @Test + void channelOpenConfirm() { + // Arrange + channelOpenTry(); + MsgChannelOpenConfirm msg = new MsgChannelOpenConfirm(); + + msg.portId = portId; + msg.channelId = channelId; + msg.proofAck = new byte[0]; + msg.proofHeight = proofHeight; + + Counterparty expectedCounterparty = new Counterparty(); + expectedCounterparty.setPortId(msg.portId); + expectedCounterparty.setChannelId(msg.channelId); + + Channel counterpartyChannel = new Channel(); + counterpartyChannel.setState(Channel.State.STATE_OPEN); + counterpartyChannel.setOrdering(baseChannel.getOrdering()); + counterpartyChannel.setCounterparty(expectedCounterparty); + counterpartyChannel.setConnectionHops(new String[]{baseConnection.getCounterparty().getConnectionId()}); + counterpartyChannel.setVersion(baseChannel.getVersion()); + + when(lightClient.mock.verifyMembership(clientId, msg.proofHeight, BigInteger.ZERO, BigInteger.ZERO, + msg.proofAck, prefix.getKeyPrefix(), IBCCommitment.channelPath(portId, channelId), + counterpartyChannel.toBytes())).thenReturn(true); + + // Act + channel.invoke(owner, "channelOpenConfirm", msg); + + // Assert + byte[] key = IBCCommitment.channelCommitmentKey(portId, channelId); + byte[] storedCommitment = (byte[]) channel.call("getCommitment", key); + + Channel expectedChannel = baseChannel; + expectedChannel.setState(Channel.State.STATE_OPEN); + assertArrayEquals(IBCCommitment.keccak256(expectedChannel.toBytes()), storedCommitment); + } + + @Test + void channelCloseInit() { + // Arrange + channelOpenConfirm(); + MsgChannelCloseInit msg = new MsgChannelCloseInit(); + + msg.portId = portId; + msg.channelId = channelId; + + // Act + channel.invoke(owner, "channelCloseInit", msg); + + // Assert + byte[] key = IBCCommitment.channelCommitmentKey(portId, channelId); + byte[] storedCommitment = (byte[]) channel.call("getCommitment", key); + + Channel expectedChannel = baseChannel; + expectedChannel.setState(Channel.State.STATE_CLOSED); + assertArrayEquals(IBCCommitment.keccak256(expectedChannel.toBytes()), storedCommitment); + } + + @Test + void channelCloseConfirm() { + // Arrange + channelOpenConfirm(); + MsgChannelCloseConfirm msg = new MsgChannelCloseConfirm(); + + msg.portId = portId; + msg.channelId = channelId; + msg.proofInit = new byte[0]; + msg.proofHeight = proofHeight; + + Counterparty expectedCounterparty = new Counterparty(); + expectedCounterparty.setPortId(msg.portId); + expectedCounterparty.setChannelId(msg.channelId); + + Channel counterpartyChannel = new Channel(); + counterpartyChannel.setState(Channel.State.STATE_CLOSED); + counterpartyChannel.setOrdering(baseChannel.getOrdering()); + counterpartyChannel.setCounterparty(expectedCounterparty); + counterpartyChannel.setConnectionHops(new String[]{baseConnection.getCounterparty().getConnectionId()}); + counterpartyChannel.setVersion(baseChannel.getVersion()); + + when(lightClient.mock.verifyMembership(clientId, msg.proofHeight, BigInteger.ZERO, BigInteger.ZERO, + msg.proofInit, prefix.getKeyPrefix(), IBCCommitment.channelPath(portId, channelId), + counterpartyChannel.toBytes())).thenReturn(true); + + // Act + channel.invoke(owner, "channelCloseConfirm", msg); + + // Assert + byte[] key = IBCCommitment.channelCommitmentKey(portId, channelId); + byte[] storedCommitment = (byte[]) channel.call("getCommitment", key); + + Channel expectedChannel = baseChannel; + expectedChannel.setState(Channel.State.STATE_CLOSED); + assertArrayEquals(IBCCommitment.keccak256(expectedChannel.toBytes()), storedCommitment); + } + + private void addConnection(String connectionId, ConnectionEnd connectionEnd) { + channel.invoke(owner, "setConnectionEnd", connectionId, connectionEnd); + } + +} diff --git a/contracts/javascore/ibc/src/test/java/ibc/ics04/channel/PacketTest.java b/contracts/javascore/ibc/src/test/java/ibc/ics04/channel/PacketTest.java new file mode 100644 index 000000000..f240ad1ee --- /dev/null +++ b/contracts/javascore/ibc/src/test/java/ibc/ics04/channel/PacketTest.java @@ -0,0 +1,583 @@ +package ibc.ics04.channel; + +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 ibc.icon.interfaces.ILightClient; +import ibc.icon.interfaces.ILightClientScoreInterface; +import ibc.icon.score.util.StringUtil; +import ibc.icon.structs.messages.MsgPacketAcknowledgement; +import ibc.icon.structs.messages.MsgPacketRecv; +import ibc.icon.structs.proto.core.channel.Channel; +import ibc.icon.structs.proto.core.channel.Counterparty; +import ibc.icon.structs.proto.core.channel.Packet; +import ibc.icon.structs.proto.core.client.Height; +import ibc.icon.structs.proto.core.commitment.MerklePrefix; +import ibc.icon.structs.proto.core.connection.ConnectionEnd; +import ibc.icon.structs.proto.core.connection.Version; +import ibc.icon.test.MockContract; +import ibc.ics03.connection.IBCConnection; +import ibc.ics24.host.IBCCommitment; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import score.Address; + +import java.math.BigInteger; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +public class PacketTest extends TestBase { + + private final ServiceManager sm = getServiceManager(); + private final Account owner = sm.createAccount(); + private Score packet; + private MockContract lightClient; + + Height proofHeight = new Height(); + String clientId = "clientId"; + String connectionId = "connectionId"; + ConnectionEnd baseConnection = new ConnectionEnd(); + Channel baseChannel = new Channel(); + MerklePrefix prefix = new MerklePrefix(); + Version version = new Version(); + ibc.icon.structs.proto.core.connection.Counterparty connectionCounterparty = + new ibc.icon.structs.proto.core.connection.Counterparty(); + Counterparty baseCounterparty = new Counterparty(); + String portId = "portId"; + String channelId = "channel-0"; + String channelVersion = IBCConnection.v1Identifier; + + Height timeOutHeight = new Height(); + Packet basePacket = new Packet(); + + public static class PacketMock extends IBCPacket { + public PacketMock() { + } + + public void setConnection(String connectionId, ConnectionEnd connectionEnd) { + connections.set(connectionId, connectionEnd); + } + + public void setClient(String clientId, Address client) { + clientImplementations.set(clientId, client); + } + + public void setChannel(String portId, String channelId, Channel channel) { + channels.at(portId).set(channelId, channel); + nextSequenceSends.at(portId).set(channelId, BigInteger.ONE); + nextSequenceReceives.at(portId).set(channelId, BigInteger.ONE); + nextSequenceAcknowledgements.at(portId).set(channelId, BigInteger.ONE); + + } + } + + @BeforeEach + public void setup() throws Exception { + packet = sm.deploy(owner, PacketMock.class); + + lightClient = new MockContract<>(ILightClientScoreInterface.class, ILightClient.class, sm, owner); + + prefix.setKeyPrefix(IBCConnection.commitmentPrefix); + proofHeight.revisionHeight = BigInteger.valueOf(5); + proofHeight.revisionNumber = BigInteger.valueOf(6); + + connectionCounterparty.setClientId(clientId); + connectionCounterparty.setConnectionId(""); + connectionCounterparty.setPrefix(prefix); + version.identifier = IBCConnection.v1Identifier; + version.features = IBCConnection.supportedV1Features; + + baseConnection.setClientId(clientId); + baseConnection.setState(ConnectionEnd.State.STATE_OPEN); + baseConnection.setCounterparty(connectionCounterparty); + baseConnection.setDelayPeriod(BigInteger.ONE); + baseConnection.setVersions(new Version[]{version}); + + baseCounterparty.setPortId(portId); + baseCounterparty.setChannelId(channelId); + + baseChannel.setState(Channel.State.STATE_OPEN); + baseChannel.setOrdering(Channel.Order.ORDER_ORDERED); + baseChannel.setCounterparty(baseCounterparty); + baseChannel.setConnectionHops(new String[]{connectionId}); + baseChannel.setVersion("v1"); + + packet.invoke(owner, "setClient", clientId, lightClient.getAddress()); + packet.invoke(owner, "setConnection", connectionId, baseConnection); + packet.invoke(owner, "setChannel", portId, channelId, baseChannel); + + basePacket.setData("ZGF0YQ=="); + basePacket.setSourcePort(portId); + basePacket.setSourceChannel(channelId); + basePacket.setDestinationPort(baseChannel.getCounterparty().getPortId()); + basePacket.setDestinationChannel(baseChannel.getCounterparty().getChannelId()); + basePacket.setSequence(BigInteger.ONE); + timeOutHeight.setRevisionHeight(BigInteger.TEN.pow(30)); + timeOutHeight.setRevisionNumber(BigInteger.TEN.pow(30)); + basePacket.setTimeoutHeight(timeOutHeight); + basePacket.setTimeoutTimestamp(BigInteger.TEN.pow(30)); + } + + @Test + void sendPacket_nonOpenChannel() { + // Arrange + baseChannel.setState(Channel.State.STATE_CLOSED); + packet.invoke(owner, "setChannel", portId, channelId, baseChannel); + + // Act & Assert + String expectedErrorMessage = "channel state must be OPEN"; + Executable closedChannel = () -> packet.invoke(owner, "sendPacket", basePacket); + AssertionError e = assertThrows(AssertionError.class, + closedChannel); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void sendPacket_wrongPort() { + // Arrange + basePacket.setDestinationPort("other port"); + + // Act & Assert + String expectedErrorMessage = "packet destination port doesn't match the counterparty's port"; + Executable wrongPort = () -> packet.invoke(owner, "sendPacket", basePacket); + AssertionError e = assertThrows(AssertionError.class, + wrongPort); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void sendPacket_wrongChannelId() { + // Arrange + basePacket.setDestinationChannel("other channel id"); + + // Act & Assert + String expectedErrorMessage = "packet destination channel doesn't match the counterparty's channel"; + Executable wrongChannelId = () -> packet.invoke(owner, "sendPacket", basePacket); + AssertionError e = assertThrows(AssertionError.class, + wrongChannelId); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void sendPacket_lowTimeoutHeight() { + // Arrange + Height latestHeight = new Height(); + latestHeight.setRevisionHeight(BigInteger.ZERO); + latestHeight.setRevisionNumber(BigInteger.TEN); + timeOutHeight.setRevisionNumber(BigInteger.ONE); + basePacket.setTimeoutHeight(timeOutHeight); + + when(lightClient.mock.getLatestHeight(clientId)).thenReturn(latestHeight); + + // Act & Assert + String expectedErrorMessage = "receiving chain block height >= packet timeout height"; + Executable lowTimeoutHeight = () -> packet.invoke(owner, "sendPacket", basePacket); + AssertionError e = assertThrows(AssertionError.class, + lowTimeoutHeight); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void sendPacket_noConsensusState() { + // Arrange + Height latestHeight = new Height(); + latestHeight.setRevisionHeight(BigInteger.ZERO); + latestHeight.setRevisionNumber(BigInteger.ZERO); + + when(lightClient.mock.getLatestHeight(clientId)).thenReturn(latestHeight); + + // Act & Assert + String expectedErrorMessage = "consensusState not found"; + Executable noConsensusState = () -> packet.invoke(owner, "sendPacket", basePacket); + AssertionError e = assertThrows(AssertionError.class, + noConsensusState); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void sendPacket_toLowBlockTimestamp() { + // Arrange + Height latestHeight = new Height(); + latestHeight.setRevisionHeight(BigInteger.ZERO); + latestHeight.setRevisionNumber(BigInteger.ZERO); + BigInteger destinationChainBlockTimestamp = BigInteger.TEN; + basePacket.setTimeoutTimestamp(destinationChainBlockTimestamp.subtract(BigInteger.ONE)); + when(lightClient.mock.getLatestHeight(clientId)).thenReturn(latestHeight); + when(lightClient.mock.getTimestampAtHeight(clientId, latestHeight)).thenReturn(destinationChainBlockTimestamp); + + // Act & Assert + String expectedErrorMessage = "receiving chain block timestamp >= packet timeout timestamp"; + Executable toLowBlockTimestamp = () -> packet.invoke(owner, "sendPacket", basePacket); + AssertionError e = assertThrows(AssertionError.class, + toLowBlockTimestamp); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void sendPacket_wrongSequence() { + // Arrange + Height latestHeight = new Height(); + latestHeight.setRevisionHeight(BigInteger.ZERO); + latestHeight.setRevisionNumber(BigInteger.ZERO); + when(lightClient.mock.getLatestHeight(clientId)).thenReturn(latestHeight); + when(lightClient.mock.getTimestampAtHeight(clientId, latestHeight)).thenReturn(BigInteger.ZERO); + basePacket.setSequence(BigInteger.TEN); + + // Act & Assert + String expectedErrorMessage = "packet sequence != next send sequence"; + Executable wrongSequence = () -> packet.invoke(owner, "sendPacket", basePacket); + AssertionError e = assertThrows(AssertionError.class, + wrongSequence); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void sendPacket() { + // Arrange + Height latestHeight = new Height(); + latestHeight.setRevisionHeight(BigInteger.ZERO); + latestHeight.setRevisionNumber(BigInteger.ZERO); + when(lightClient.mock.getLatestHeight(clientId)).thenReturn(latestHeight); + when(lightClient.mock.getTimestampAtHeight(clientId, latestHeight)).thenReturn(BigInteger.ZERO); + + // Act + packet.invoke(owner, "sendPacket", basePacket); + basePacket.setSequence(BigInteger.TWO); + packet.invoke(owner, "sendPacket", basePacket); + + // Assert + byte[] key1 = IBCCommitment.packetCommitmentKey(basePacket.getSourcePort(), + basePacket.getSourceChannel(), + BigInteger.ONE); + byte[] key2 = IBCCommitment.packetCommitmentKey(basePacket.getSourcePort(), + basePacket.getSourceChannel(), + basePacket.getSequence()); + + byte[] storedCommitment1 = (byte[]) packet.call("getCommitment", key1); + byte[] storedCommitment2 = (byte[]) packet.call("getCommitment", key2); + + byte[] expectedCommitment = IBCCommitment.keccak256( + IBCCommitment.sha256( + StringUtil.encodePacked( + basePacket.getTimeoutTimestamp(), + basePacket.getTimeoutHeight().getRevisionNumber(), + basePacket.getTimeoutHeight().getRevisionHeight(), + basePacket.getData()))); + + assertArrayEquals(expectedCommitment, storedCommitment1); + assertArrayEquals(expectedCommitment, storedCommitment2); + assertEquals(BigInteger.valueOf(3), + packet.call("getNextSequenceSend", basePacket.getSourcePort(), basePacket.getSourceChannel())); + } + + @Test + void recvPacket_nonOpenChannel() { + // Arrange + baseChannel.setState(Channel.State.STATE_CLOSED); + packet.invoke(owner, "setChannel", portId, channelId, baseChannel); + + MsgPacketRecv msg = new MsgPacketRecv(); + msg.packet = basePacket; + + // Act & Assert + String expectedErrorMessage = "channel state must be OPEN"; + Executable closedChannel = () -> packet.invoke(owner, "recvPacket", msg); + AssertionError e = assertThrows(AssertionError.class, + closedChannel); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void recvPacket_wrongPort() { + // Arrange + basePacket.setDestinationPort("other port"); + MsgPacketRecv msg = new MsgPacketRecv(); + msg.packet = basePacket; + + // Act & Assert + String expectedErrorMessage = "packet destination port doesn't match the counterparty's port"; + Executable wrongPort = () -> packet.invoke(owner, "recvPacket", msg); + AssertionError e = assertThrows(AssertionError.class, + wrongPort); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void recvPacket_wrongChannelId() { + // Arrange + basePacket.setDestinationChannel("other channel id"); + MsgPacketRecv msg = new MsgPacketRecv(); + msg.packet = basePacket; + + // Act & Assert + String expectedErrorMessage = "packet destination channel doesn't match the counterparty's channel"; + Executable wrongChannelId = () -> packet.invoke(owner, "recvPacket", msg); + AssertionError e = assertThrows(AssertionError.class, + wrongChannelId); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void recvPacket_nonOpenConnection() { + // Arrange + baseConnection.setState(ConnectionEnd.State.STATE_TRYOPEN); + packet.invoke(owner, "setConnection", connectionId, baseConnection); + + MsgPacketRecv msg = new MsgPacketRecv(); + msg.packet = basePacket; + + // Act & Assert + String expectedErrorMessage = "connection state is not OPEN"; + Executable nonOpenConnection = () -> packet.invoke(owner, "recvPacket", msg); + AssertionError e = assertThrows(AssertionError.class, + nonOpenConnection); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void recvPacket_lowTimeoutHeight() { + // Arrange + timeOutHeight.setRevisionHeight(BigInteger.valueOf(sm.getBlock().getHeight())); + basePacket.setTimeoutHeight(timeOutHeight); + MsgPacketRecv msg = new MsgPacketRecv(); + msg.packet = basePacket; + + // Act & Assert + String expectedErrorMessage = "block height >= packet timeout height"; + Executable lowTimeoutHeight = () -> packet.invoke(owner, "recvPacket", msg); + AssertionError e = assertThrows(AssertionError.class, + lowTimeoutHeight); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void recvPacket_lowTimeoutTimestamp() { + // Arrange + basePacket.setTimeoutTimestamp(BigInteger.valueOf(sm.getBlock().getTimestamp())); + MsgPacketRecv msg = new MsgPacketRecv(); + msg.packet = basePacket; + + // Act & Assert + String expectedErrorMessage = "block timestamp >= packet timeout timestamp"; + Executable lowTimeoutTimestamp = () -> packet.invoke(owner, "recvPacket", msg); + AssertionError e = assertThrows(AssertionError.class, + lowTimeoutTimestamp); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void recvPacket_doubleReceive_UnOrdered() { + // Arrange + baseChannel.setOrdering(Channel.Order.ORDER_UNORDERED); + packet.invoke(owner, "setChannel", portId, channelId, baseChannel); + MsgPacketRecv msg = new MsgPacketRecv(); + msg.packet = basePacket; + msg.proof = new byte[1]; + msg.proofHeight = proofHeight; + + byte[] commitmentPath = IBCCommitment.packetCommitmentPath(msg.packet.getSourcePort(), + msg.packet.getSourceChannel(), msg.packet.getSequence()); + byte[] commitmentBytes = IBCCommitment.keccak256( + IBCCommitment.sha256( + StringUtil.encodePacked( + basePacket.getTimeoutTimestamp(), + basePacket.getTimeoutHeight().getRevisionNumber(), + basePacket.getTimeoutHeight().getRevisionHeight(), + basePacket.getData()))); + + when(lightClient.mock.verifyMembership(clientId, proofHeight, baseConnection.getDelayPeriod(), BigInteger.ZERO, + msg.proof, prefix.getKeyPrefix(), commitmentPath, commitmentBytes)).thenReturn(true); + + packet.invoke(owner, "recvPacket", msg); + + // Act & Assert + String expectedErrorMessage = "packet sequence already has been received"; + Executable alreadyReceived = () -> packet.invoke(owner, "recvPacket", msg); + AssertionError e = assertThrows(AssertionError.class, + alreadyReceived); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void recvPacket_outOfOrder_UnOrdered() { + // Arrange + baseChannel.setOrdering(Channel.Order.ORDER_UNORDERED); + packet.invoke(owner, "setChannel", portId, channelId, baseChannel); + MsgPacketRecv msg = new MsgPacketRecv(); + msg.packet = basePacket; + msg.proof = new byte[1]; + msg.proofHeight = proofHeight; + + byte[] commitmentPath1 = IBCCommitment.packetCommitmentPath(msg.packet.getSourcePort(), + msg.packet.getSourceChannel(), msg.packet.getSequence()); + byte[] commitmentPath2 = IBCCommitment.packetCommitmentPath(msg.packet.getSourcePort(), + msg.packet.getSourceChannel(), msg.packet.getSequence().add(BigInteger.ONE)); + byte[] commitmentBytes = IBCCommitment.keccak256( + IBCCommitment.sha256( + StringUtil.encodePacked( + basePacket.getTimeoutTimestamp(), + basePacket.getTimeoutHeight().getRevisionNumber(), + basePacket.getTimeoutHeight().getRevisionHeight(), + basePacket.getData()))); + + when(lightClient.mock.verifyMembership(clientId, proofHeight, baseConnection.getDelayPeriod(), BigInteger.ZERO, + msg.proof, prefix.getKeyPrefix(), commitmentPath1, commitmentBytes)).thenReturn(true); + when(lightClient.mock.verifyMembership(clientId, proofHeight, baseConnection.getDelayPeriod(), BigInteger.ZERO, + msg.proof, prefix.getKeyPrefix(), commitmentPath2, commitmentBytes)).thenReturn(true); + // Act + msg.packet.setSequence(BigInteger.TWO); + packet.invoke(owner, "recvPacket", msg); + // Assert + msg.packet.setSequence(BigInteger.ONE); + packet.invoke(owner, "recvPacket", msg); + } + + @Test + void recvPacket_futureReceive_Ordered() { + // Arrange + baseChannel.setOrdering(Channel.Order.ORDER_ORDERED); + packet.invoke(owner, "setChannel", portId, channelId, baseChannel); + MsgPacketRecv msg = new MsgPacketRecv(); + msg.packet = basePacket; + msg.packet.setSequence(BigInteger.TWO); + msg.proof = new byte[1]; + msg.proofHeight = proofHeight; + + byte[] commitmentPath = IBCCommitment.packetCommitmentPath(msg.packet.getSourcePort(), + msg.packet.getSourceChannel(), msg.packet.getSequence()); + byte[] commitmentBytes = IBCCommitment.keccak256( + IBCCommitment.sha256( + StringUtil.encodePacked( + basePacket.getTimeoutTimestamp(), + basePacket.getTimeoutHeight().getRevisionNumber(), + basePacket.getTimeoutHeight().getRevisionHeight(), + basePacket.getData()))); + + when(lightClient.mock.verifyMembership(clientId, proofHeight, baseConnection.getDelayPeriod(), BigInteger.ZERO, + msg.proof, prefix.getKeyPrefix(), commitmentPath, commitmentBytes)).thenReturn(true); + + // Act & Assert + String expectedErrorMessage = "packet sequence != next receive sequence"; + Executable notNext = () -> packet.invoke(owner, "recvPacket", msg); + AssertionError e = assertThrows(AssertionError.class, + notNext); + assertTrue(e.getMessage().contains(expectedErrorMessage)); + } + + @Test + void recvPacket_UnOrdered() { + // Arrange + baseChannel.setOrdering(Channel.Order.ORDER_UNORDERED); + packet.invoke(owner, "setChannel", portId, channelId, baseChannel); + MsgPacketRecv msg = new MsgPacketRecv(); + msg.packet = basePacket; + msg.proof = new byte[1]; + msg.proofHeight = proofHeight; + + byte[] commitmentPath = IBCCommitment.packetCommitmentPath(msg.packet.getSourcePort(), + msg.packet.getSourceChannel(), msg.packet.getSequence()); + byte[] commitmentBytes = IBCCommitment.keccak256( + IBCCommitment.sha256( + StringUtil.encodePacked( + basePacket.getTimeoutTimestamp(), + basePacket.getTimeoutHeight().getRevisionNumber(), + basePacket.getTimeoutHeight().getRevisionHeight(), + basePacket.getData()))); + + when(lightClient.mock.verifyMembership(clientId, proofHeight, baseConnection.getDelayPeriod(), BigInteger.ZERO, + msg.proof, prefix.getKeyPrefix(), commitmentPath, commitmentBytes)).thenReturn(true); + // Act + packet.invoke(owner, "recvPacket", msg); + + // Assert + assertEquals(BigInteger.ONE, packet.call("getPacketReceipt", portId, channelId, basePacket.getSequence())); + } + + @Test + void recvPacket_Ordered() { + // Arrange + baseChannel.setOrdering(Channel.Order.ORDER_ORDERED); + packet.invoke(owner, "setChannel", portId, channelId, baseChannel); + MsgPacketRecv msg = new MsgPacketRecv(); + msg.packet = basePacket; + msg.proof = new byte[1]; + msg.proofHeight = proofHeight; + + byte[] commitmentPath = IBCCommitment.packetCommitmentPath(msg.packet.getSourcePort(), + msg.packet.getSourceChannel(), msg.packet.getSequence()); + byte[] commitmentBytes = IBCCommitment.keccak256( + IBCCommitment.sha256( + StringUtil.encodePacked( + basePacket.getTimeoutTimestamp(), + basePacket.getTimeoutHeight().getRevisionNumber(), + basePacket.getTimeoutHeight().getRevisionHeight(), + basePacket.getData()))); + + when(lightClient.mock.verifyMembership(clientId, proofHeight, baseConnection.getDelayPeriod(), BigInteger.ZERO, + msg.proof, prefix.getKeyPrefix(), commitmentPath, commitmentBytes)).thenReturn(true); + // Act + packet.invoke(owner, "recvPacket", msg); + + // Assert + assertEquals(basePacket.getSequence().add(BigInteger.ONE), + packet.call("getNextSequenceReceive", portId, channelId)); + } + + @Test + void writeAcknowledgement() { + // Arrange + byte[] acknowledgement = new byte[5]; + BigInteger sequence = BigInteger.ONE; + + // Act + packet.invoke(owner, "writeAcknowledgement", baseCounterparty.getPortId(), baseCounterparty.getChannelId(), + sequence, acknowledgement); + + // Assert + byte[] ackCommitmentKey = IBCCommitment.packetAcknowledgementCommitmentKey(baseCounterparty.getPortId(), + baseCounterparty.getChannelId(), sequence); + byte[] storedCommitment = (byte[]) packet.call("getCommitment", ackCommitmentKey); + + byte[] expectedCommitment = IBCCommitment.keccak256(IBCCommitment.sha256(acknowledgement)); + assertArrayEquals(expectedCommitment, storedCommitment); + } + + @Test + void acknowledgePacket() { + // Arrange + Height latestHeight = new Height(); + latestHeight.setRevisionHeight(BigInteger.ZERO); + latestHeight.setRevisionNumber(BigInteger.ZERO); + when(lightClient.mock.getLatestHeight(clientId)).thenReturn(latestHeight); + when(lightClient.mock.getTimestampAtHeight(clientId, latestHeight)).thenReturn(BigInteger.ZERO); + + packet.invoke(owner, "sendPacket", basePacket); + + MsgPacketAcknowledgement msg = new MsgPacketAcknowledgement(); + msg.packet = basePacket; + msg.acknowledgement = new byte[4]; + msg.proof = new byte[5]; + msg.proofHeight = proofHeight; + + byte[] commitmentPath = IBCCommitment.packetAcknowledgementCommitmentPath(msg.packet.destinationPort, + msg.packet.destinationChannel, msg.packet.sequence); + when(lightClient.mock.verifyMembership(clientId, proofHeight, + baseConnection.getDelayPeriod(), BigInteger.ZERO, + msg.proof, prefix.getKeyPrefix(), commitmentPath, + IBCCommitment.sha256(msg.acknowledgement))).thenReturn(true); + + // Act + packet.invoke(owner, "acknowledgePacket", msg); + + // Assert + byte[] packetCommitmentKey = IBCCommitment.packetCommitmentKey(msg.packet.getSourcePort(), + msg.packet.getSourceChannel(), msg.packet.getSequence()); + Object storedCommitment = packet.call("getCommitment", packetCommitmentKey); + assertNull(storedCommitment); + assertEquals(BigInteger.TWO, packet.call("getNextSequenceAcknowledgement", basePacket.getSourcePort(), + basePacket.getSourceChannel())); + } +} diff --git a/contracts/javascore/lib/src/main/java/ibc/icon/interfaces/IIBCChannelHandshake.java b/contracts/javascore/lib/src/main/java/ibc/icon/interfaces/IIBCChannelHandshake.java index 4bf0f224b..338acd00d 100644 --- a/contracts/javascore/lib/src/main/java/ibc/icon/interfaces/IIBCChannelHandshake.java +++ b/contracts/javascore/lib/src/main/java/ibc/icon/interfaces/IIBCChannelHandshake.java @@ -1,5 +1,44 @@ package ibc.icon.interfaces; +import ibc.icon.structs.messages.*; public interface IIBCChannelHandshake { + /** + * {@code @dev} channelOpenInit is called by a module to initiate a channel opening + * handshake with a module on another chain. + */ + String channelOpenInit(MsgChannelOpenInit msg); + + /** + * {@code @dev} channelOpenTry is called by a module to accept the first step of a + * channel opening handshake initiated by a module on another chain. + */ + String channelOpenTry(MsgChannelOpenTry msg); + + /** + * {@code @dev} channelOpenAck is called by the handshake-originating module to + * acknowledge the acceptance of the initial request by the counterparty + * module on the other chain. + */ + void channelOpenAck(MsgChannelOpenAck msg); + + /** + * {@code @dev} channelOpenConfirm is called by the counterparty module to close their + * end of the channel, since the other end has been closed. + */ + void channelOpenConfirm(MsgChannelOpenConfirm msg); + + /** + * {@code @dev} channelCloseInit is called by either module to close their end of the + * channel. Once closed, channels cannot be reopened. + */ + void channelCloseInit(MsgChannelCloseInit msg); + + /** + * {@code @dev} channelCloseConfirm is called by the counterparty module to close their + * end of the + * channel, since the other end has been closed. + */ + void channelCloseConfirm(MsgChannelCloseConfirm msg); + } diff --git a/contracts/javascore/lib/src/main/java/ibc/icon/interfaces/IIBCClient.java b/contracts/javascore/lib/src/main/java/ibc/icon/interfaces/IIBCClient.java index 5be7a49e2..cd8be28d8 100644 --- a/contracts/javascore/lib/src/main/java/ibc/icon/interfaces/IIBCClient.java +++ b/contracts/javascore/lib/src/main/java/ibc/icon/interfaces/IIBCClient.java @@ -1,23 +1,26 @@ package ibc.icon.interfaces; -import score.Address; - import ibc.icon.structs.messages.MsgCreateClient; import ibc.icon.structs.messages.MsgUpdateClient; +import score.Address; public interface IIBCClient { /** - * @dev registerClient registers a new client type into the client registry + * {@code @dev} registerClient registers a new client type into the client registry + * @param clientType Type of client + * @param lightClient Light client contract address */ void registerClient(String clientType, Address lightClient); /** - * @dev createClient creates a new client state and populates it with a given consensus state + * {@code @dev} createClient creates a new client state and populates it with a given + * consensus state */ - void createClient(MsgCreateClient msg); + String createClient(MsgCreateClient msg); /** - * @dev updateClient updates the consensus state and the state root from a provided header + * {@code @dev} updateClient updates the consensus state and the state root from a + * provided header */ void updateClient(MsgUpdateClient msg); } diff --git a/contracts/javascore/lib/src/main/java/ibc/icon/interfaces/IIBCConnection.java b/contracts/javascore/lib/src/main/java/ibc/icon/interfaces/IIBCConnection.java index 545b4cbd7..434665771 100644 --- a/contracts/javascore/lib/src/main/java/ibc/icon/interfaces/IIBCConnection.java +++ b/contracts/javascore/lib/src/main/java/ibc/icon/interfaces/IIBCConnection.java @@ -1,5 +1,17 @@ package ibc.icon.interfaces; +import ibc.icon.structs.messages.MsgConnectionOpenAck; +import ibc.icon.structs.messages.MsgConnectionOpenConfirm; +import ibc.icon.structs.messages.MsgConnectionOpenInit; +import ibc.icon.structs.messages.MsgConnectionOpenTry; public interface IIBCConnection { + + public String connectionOpenInit(MsgConnectionOpenInit msg); + + public String connectionOpenTry(MsgConnectionOpenTry msg); + + public void connectionOpenAck(MsgConnectionOpenAck msg); + + public void connectionOpenConfirm(MsgConnectionOpenConfirm msg); } diff --git a/contracts/javascore/lib/src/main/java/ibc/icon/interfaces/IIBCPacket.java b/contracts/javascore/lib/src/main/java/ibc/icon/interfaces/IIBCPacket.java index 6797d83ff..88ac84b4e 100644 --- a/contracts/javascore/lib/src/main/java/ibc/icon/interfaces/IIBCPacket.java +++ b/contracts/javascore/lib/src/main/java/ibc/icon/interfaces/IIBCPacket.java @@ -1,5 +1,40 @@ package ibc.icon.interfaces; +import ibc.icon.structs.messages.MsgPacketAcknowledgement; +import ibc.icon.structs.messages.MsgPacketRecv; +import ibc.icon.structs.proto.core.channel.Packet; + +import java.math.BigInteger; public interface IIBCPacket { + + /** + * {@code @dev} sendPacket is called by a module in order to send an IBC packet on a + * channel. + * The packet sequence generated for the packet to be sent is returned. An + * error is returned if one occurs. + */ + void sendPacket(Packet packet); + + /** + * {@code @dev} recvPacket is called by a module in order to receive & process an IBC + * packet sent on the corresponding channel end on the counterparty chain. + */ + void recvPacket(MsgPacketRecv msg); + + /** + * {@code @dev} writeAcknowledgement writes the packet execution acknowledgement to the + * state,which will be verified by the counterparty chain using AcknowledgePacket. + */ + void writeAcknowledgement(String destinationPortId, String destinationChannel, BigInteger sequence, + byte[] acknowledgement); + + /** + * {@code @dev} AcknowledgePacket is called by a module to process the acknowledgement + * of a packet previously sent by the calling module on a channel to a counterparty module on the counterparty + * chain. Its intended usage is within the ante handler. AcknowledgePacket will clean up the packet commitment, + * which is no longer necessary since the packet has been received and acted upon. It will also increment + * NextSequenceAck in case of ORDERED channels. + */ + void acknowledgePacket(MsgPacketAcknowledgement msg); } diff --git a/contracts/javascore/lib/src/main/java/ibc/icon/interfaces/ILightClient.java b/contracts/javascore/lib/src/main/java/ibc/icon/interfaces/ILightClient.java new file mode 100644 index 000000000..4c6c6c4ef --- /dev/null +++ b/contracts/javascore/lib/src/main/java/ibc/icon/interfaces/ILightClient.java @@ -0,0 +1,94 @@ +package ibc.icon.interfaces; + +import foundation.icon.score.client.ScoreInterface; +import ibc.icon.structs.messages.CreateClientResponse; +import ibc.icon.structs.messages.UpdateClientResponse; +import ibc.icon.structs.proto.core.client.Height; + +import java.math.BigInteger; + +/** + * {@code @dev} This defines an interface for Light Client contract can be integrated + * with ibc-solidity. + * You can register the Light Client contract that implements this through + * `registerClient` on IBCHandler. + */ +@ScoreInterface +public interface ILightClient { + /** + * {@code @dev} createClient creates a new client with the given state. + * If succeeded, it returns a commitment for the initial state. + */ + CreateClientResponse createClient(String clientId, byte[] clientStateBytes, byte[] consensusStateBytes); + + /** + * {@code @dev} getTimestampAtHeight returns the timestamp of the consensus state at the + * given height. + */ + BigInteger getTimestampAtHeight(String clientId, Height height); + + /** + * {@code @dev} getLatestHeight returns the latest height of the client state + * corresponding to `clientId`. + */ + Height getLatestHeight(String clientId); + + /** + * {@code @dev} updateClient updates the client corresponding to `clientId`. + * If succeeded, it returns a commitment for the updated state. + * If there are no updates for consensus state, this public void should return an empty array as `updates`. + *

+ * NOTE: updateClient is intended to perform the followings: + * 1. verify a given client message(e.g. header) + * 2. check misbehaviour such like duplicate block height + * 3. if misbehaviour is found, update state accordingly and return + * 4. update state(s) with the client message + * 5. persist the state(s) on the host + */ + UpdateClientResponse updateClient(String clientId, byte[] clientMessageBytes); + + /** + * {@code @dev} verifyMembership is a generic proof verification method which verifies a + * proof of the existence of a value at a given CommitmentPath at the + * specified height. + * The caller is expected to construct the full CommitmentPath from a + * CommitmentPrefix and a standardized path (as defined in ICS 24). + */ + Boolean verifyMembership( + String clientId, + Height height, + BigInteger delayTimePeriod, + BigInteger delayBlockPeriod, + byte[] proof, + String prefix, + byte[] path, + byte[] value); + + /** + * {@code @dev} verifyNonMembership is a generic proof verification method which + * verifies the absence of a given CommitmentPath at a specified height. + * The caller is expected to construct the full CommitmentPath from a + * CommitmentPrefix and a standardized path (as defined in ICS 24). + */ + Boolean verifyNonMembership( + String clientId, + Height height, + BigInteger delayTimePeriod, + BigInteger delayBlockPeriod, + byte[] proof, + String prefix, + byte[] path); + + /** + * {@code @dev} getClientState returns the clientState corresponding to `clientId`. + * If it's not found, the public void returns false. + */ + byte[] getClientState(String clientId); + + /** + * {@code @dev} getConsensusState returns the consensusState corresponding to `clientId` + * and `height`. + * If it's not found, the public void returns false. + */ + byte[] getConsensusState(String clientId, Height height); +} \ No newline at end of file diff --git a/contracts/javascore/lib/src/main/java/ibc/icon/structs/messages/ConsensusStateUpdate.java b/contracts/javascore/lib/src/main/java/ibc/icon/structs/messages/ConsensusStateUpdate.java new file mode 100644 index 000000000..a96ba3058 --- /dev/null +++ b/contracts/javascore/lib/src/main/java/ibc/icon/structs/messages/ConsensusStateUpdate.java @@ -0,0 +1,15 @@ +package ibc.icon.structs.messages; + +import ibc.icon.structs.proto.core.client.Height; + +public class ConsensusStateUpdate { + public ConsensusStateUpdate(byte[] consensusStateCommitment, Height height) { + this.consensusStateCommitment = consensusStateCommitment; + this.height = height; + } + + // commitment for updated consensusState + public byte[] consensusStateCommitment; + // updated height + public Height height; +} diff --git a/contracts/javascore/lib/src/main/java/ibc/icon/structs/messages/CreateClientResponse.java b/contracts/javascore/lib/src/main/java/ibc/icon/structs/messages/CreateClientResponse.java new file mode 100644 index 000000000..4aeef16a7 --- /dev/null +++ b/contracts/javascore/lib/src/main/java/ibc/icon/structs/messages/CreateClientResponse.java @@ -0,0 +1,13 @@ +package ibc.icon.structs.messages; + +public class CreateClientResponse { + public CreateClientResponse(byte[] clientStateCommitment, ConsensusStateUpdate update, boolean ok) { + this.clientStateCommitment = clientStateCommitment; + this.update = update; + this.ok = ok; + } + + public byte[] clientStateCommitment; + public ConsensusStateUpdate update; + public boolean ok; +} diff --git a/contracts/javascore/lib/src/main/java/ibc/icon/structs/messages/MsgConnectionOpenAck.java b/contracts/javascore/lib/src/main/java/ibc/icon/structs/messages/MsgConnectionOpenAck.java index abbd758a5..f03e52eb0 100644 --- a/contracts/javascore/lib/src/main/java/ibc/icon/structs/messages/MsgConnectionOpenAck.java +++ b/contracts/javascore/lib/src/main/java/ibc/icon/structs/messages/MsgConnectionOpenAck.java @@ -1,6 +1,5 @@ package ibc.icon.structs.messages; - import ibc.icon.structs.proto.core.client.Height; import ibc.icon.structs.proto.core.connection.Version; diff --git a/contracts/javascore/lib/src/main/java/ibc/icon/structs/messages/UpdateClientResponse.java b/contracts/javascore/lib/src/main/java/ibc/icon/structs/messages/UpdateClientResponse.java new file mode 100644 index 000000000..6ebc5b7c3 --- /dev/null +++ b/contracts/javascore/lib/src/main/java/ibc/icon/structs/messages/UpdateClientResponse.java @@ -0,0 +1,13 @@ +package ibc.icon.structs.messages; + +public class UpdateClientResponse { + public UpdateClientResponse(byte[] clientStateCommitment, ConsensusStateUpdate[] updates, boolean ok) { + this.clientStateCommitment = clientStateCommitment; + this.updates = updates; + this.ok = ok; + } + + public byte[] clientStateCommitment; + public ConsensusStateUpdate[] updates; + public boolean ok; +} diff --git a/contracts/javascore/lib/src/main/java/ibc/icon/structs/proto/core/channel/Channel.java b/contracts/javascore/lib/src/main/java/ibc/icon/structs/proto/core/channel/Channel.java index f8d96a88b..258ba0ce6 100644 --- a/contracts/javascore/lib/src/main/java/ibc/icon/structs/proto/core/channel/Channel.java +++ b/contracts/javascore/lib/src/main/java/ibc/icon/structs/proto/core/channel/Channel.java @@ -1,13 +1,13 @@ package ibc.icon.structs.proto.core.channel; -import java.util.List; - import score.ByteArrayObjectWriter; import score.Context; import score.ObjectReader; import score.ObjectWriter; import scorex.util.ArrayList; +import java.util.List; + public class Channel { // State defines if a channel is in one of the following states: @@ -115,6 +115,17 @@ public void writeObject(ObjectWriter writer) { writer.end(); } + public static Channel fromBytes(byte[] bytes) { + ObjectReader reader = Context.newByteArrayObjectReader("RLPn", bytes); + return Channel.readObject(reader); + } + + public byte[] toBytes() { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); + Channel.writeObject(writer, this); + return writer.toByteArray(); + } + public State getState() { return State.valueOf(state); } diff --git a/contracts/javascore/lib/src/main/java/ibc/icon/structs/proto/core/channel/Packet.java b/contracts/javascore/lib/src/main/java/ibc/icon/structs/proto/core/channel/Packet.java index 0263b5138..a88e79df5 100644 --- a/contracts/javascore/lib/src/main/java/ibc/icon/structs/proto/core/channel/Packet.java +++ b/contracts/javascore/lib/src/main/java/ibc/icon/structs/proto/core/channel/Packet.java @@ -1,8 +1,12 @@ package ibc.icon.structs.proto.core.channel; -import java.math.BigInteger; - import ibc.icon.structs.proto.core.client.Height; +import score.ByteArrayObjectWriter; +import score.Context; +import score.ObjectReader; +import score.ObjectWriter; + +import java.math.BigInteger; // Packet defines a type that carries data across different chains through IBC public class Packet { @@ -25,6 +29,57 @@ public class Packet { // block timestamp (in nanoseconds) after which the packet times out public BigInteger timeoutTimestamp; + public static void writeObject(ObjectWriter writer, Packet obj) { + obj.writeObject(writer); + } + + public static Packet readObject(ObjectReader reader) { + Packet obj = new Packet(); + reader.beginList(); + obj.sequence = reader.readBigInteger(); + obj.sourcePort = reader.readString(); + obj.sourceChannel = reader.readString(); + obj.destinationPort = reader.readString(); + obj.destinationChannel = reader.readString(); + obj.data = reader.readString(); + + Height timeoutHeight = new Height(); + timeoutHeight.setRevisionNumber(reader.readBigInteger()); + timeoutHeight.setRevisionHeight(reader.readBigInteger()); + + obj.timeoutHeight = timeoutHeight; + obj.timeoutTimestamp = reader.readBigInteger(); + reader.end(); + + return obj; + } + + public void writeObject(ObjectWriter writer) { + writer.beginList(9); + writer.write(this.sequence); + writer.write(this.sourcePort); + writer.write(this.sourceChannel); + writer.write(this.destinationPort); + writer.write(this.destinationChannel); + writer.write(this.data); + writer.write(this.timeoutHeight.getRevisionNumber()); + writer.write(this.timeoutHeight.getRevisionHeight()); + writer.write(this.timeoutTimestamp); + + writer.end(); + } + + public static Packet fromBytes(byte[] bytes) { + ObjectReader reader = Context.newByteArrayObjectReader("RLPn", bytes); + return Packet.readObject(reader); + } + + public byte[] toBytes() { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); + Packet.writeObject(writer, this); + return writer.toByteArray(); + } + public BigInteger getSequence() { return sequence; } diff --git a/contracts/javascore/lib/src/main/java/ibc/icon/structs/proto/core/client/Height.java b/contracts/javascore/lib/src/main/java/ibc/icon/structs/proto/core/client/Height.java index b639396fc..72719ea9b 100644 --- a/contracts/javascore/lib/src/main/java/ibc/icon/structs/proto/core/client/Height.java +++ b/contracts/javascore/lib/src/main/java/ibc/icon/structs/proto/core/client/Height.java @@ -9,13 +9,54 @@ public class Height { public BigInteger getRevisionNumber() { return revisionNumber; } + public void setRevisionNumber(BigInteger revisionNumber) { this.revisionNumber = revisionNumber; } + public BigInteger getRevisionHeight() { return revisionHeight; } + public void setRevisionHeight(BigInteger revisionHeight) { this.revisionHeight = revisionHeight; } + + // public BigInteger toUint128() { + // return (uint128(this.getRevisionNumber()) << 64) | + // uint128(this.getRevisionHeight()); + // } + + public boolean isZero() { + return this.getRevisionNumber().equals(BigInteger.ZERO) && this.getRevisionHeight().equals(BigInteger.ZERO); + } + + public boolean lt(Height other) { + return this.getRevisionNumber().compareTo(other.getRevisionNumber()) < 0 + || (this.getRevisionNumber().equals(other.getRevisionNumber()) + && this.getRevisionHeight().compareTo(other.getRevisionHeight()) < 0); + } + + public boolean lte(Height other) { + return this.getRevisionNumber().compareTo(other.getRevisionNumber()) < 0 + || (this.getRevisionNumber().equals(other.getRevisionNumber()) + && this.getRevisionHeight().compareTo(other.getRevisionHeight()) <= 0); + } + + public boolean eq(Height other) { + return this.getRevisionNumber().equals(other.getRevisionNumber()) + && this.getRevisionHeight().equals(other.getRevisionHeight()); + } + + public boolean gt(Height other) { + return this.getRevisionNumber().compareTo(other.getRevisionNumber()) > 0 + || (this.getRevisionNumber().equals(other.getRevisionNumber()) + && this.getRevisionHeight().compareTo(other.getRevisionHeight()) > 0); + } + + public boolean gte(Height other) { + return this.getRevisionNumber().compareTo(other.getRevisionNumber()) > 0 + || (this.getRevisionNumber().equals(other.getRevisionNumber()) + && this.getRevisionHeight().compareTo(other.getRevisionHeight()) >= 0); + } } diff --git a/contracts/javascore/lib/src/main/java/ibc/icon/structs/proto/core/connection/ConnectionEnd.java b/contracts/javascore/lib/src/main/java/ibc/icon/structs/proto/core/connection/ConnectionEnd.java index 93927c08f..1557c723a 100644 --- a/contracts/javascore/lib/src/main/java/ibc/icon/structs/proto/core/connection/ConnectionEnd.java +++ b/contracts/javascore/lib/src/main/java/ibc/icon/structs/proto/core/connection/ConnectionEnd.java @@ -1,14 +1,14 @@ package ibc.icon.structs.proto.core.connection; -import java.math.BigInteger; -import java.util.List; - import score.ByteArrayObjectWriter; import score.Context; import score.ObjectReader; import score.ObjectWriter; import scorex.util.ArrayList; +import java.math.BigInteger; +import java.util.List; + // ConnectionEnd defines a stateful object on a chain connected to another // separate one. // NOTE: there must only be 2 defined ConnectionEnds to establish @@ -103,6 +103,17 @@ public void writeObject(ObjectWriter writer) { writer.end(); } + public static ConnectionEnd fromBytes(byte[] bytes) { + ObjectReader reader = Context.newByteArrayObjectReader("RLPn", bytes); + return ConnectionEnd.readObject(reader); + } + + public byte[] toBytes() { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); + ConnectionEnd.writeObject(writer, this); + return writer.toByteArray(); + } + public String getClientId() { return clientId; } diff --git a/contracts/javascore/lib/src/main/java/ibc/icon/structs/proto/core/connection/Version.java b/contracts/javascore/lib/src/main/java/ibc/icon/structs/proto/core/connection/Version.java index da5764942..19a1ca249 100644 --- a/contracts/javascore/lib/src/main/java/ibc/icon/structs/proto/core/connection/Version.java +++ b/contracts/javascore/lib/src/main/java/ibc/icon/structs/proto/core/connection/Version.java @@ -1,13 +1,14 @@ package ibc.icon.structs.proto.core.connection; -import java.util.List; - import score.ByteArrayObjectWriter; import score.Context; import score.ObjectReader; import score.ObjectWriter; import scorex.util.ArrayList; +import java.util.Arrays; +import java.util.List; + public class Version { public String identifier; public String[] features; @@ -60,6 +61,10 @@ public void writeObject(ObjectWriter writer) { writer.end(); } + public boolean equals(Version v) { + return this.identifier.equals(v.identifier) && Arrays.equals(this.features, v.features); + } + public String getIdentifier() { return identifier; } diff --git a/contracts/javascore/lib/src/test/java/ibc/icon/structs/proto/core/ProtoStorageTest.java b/contracts/javascore/lib/src/test/java/ibc/icon/structs/proto/core/ProtoStorageTest.java index fbbbbe361..3df002781 100644 --- a/contracts/javascore/lib/src/test/java/ibc/icon/structs/proto/core/ProtoStorageTest.java +++ b/contracts/javascore/lib/src/test/java/ibc/icon/structs/proto/core/ProtoStorageTest.java @@ -4,20 +4,21 @@ import com.iconloop.score.test.Score; import com.iconloop.score.test.ServiceManager; import com.iconloop.score.test.TestBase; - import ibc.icon.structs.proto.core.channel.Channel; +import ibc.icon.structs.proto.core.channel.Packet; +import ibc.icon.structs.proto.core.client.Height; import ibc.icon.structs.proto.core.commitment.MerklePrefix; import ibc.icon.structs.proto.core.connection.ConnectionEnd; import ibc.icon.structs.proto.core.connection.Counterparty; import ibc.icon.structs.proto.core.connection.Version; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import score.VarDB; import java.math.BigInteger; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static score.Context.newVarDB; public class ProtoStorageTest extends TestBase { @@ -30,6 +31,7 @@ public static class DummyScore { VarDB connectionEndDB = newVarDB("connectionEndDB", ConnectionEnd.class); VarDB channelDB = newVarDB("channelDB", Channel.class); + VarDB packetDB = newVarDB("packetDB", Packet.class); public DummyScore() { } @@ -49,6 +51,14 @@ public void setChannel(Channel channel) { public Channel getChannel() { return channelDB.get(); } + + public void setPacket(Packet packet) { + packetDB.set(packet); + } + + public Packet getPacket() { + return packetDB.get(); + } } @BeforeEach @@ -71,12 +81,12 @@ public void storeConnectionEnd() { Version version2 = new Version(); version1.setIdentifier("version1"); version2.setIdentifier("version2"); - version1.setFeatures(new String[] { "v1Feature1", "v1Feature2" }); - version2.setFeatures(new String[] { "v2Feature1", "v2Feature2", "v2Feature3" }); + version1.setFeatures(new String[]{"v1Feature1", "v1Feature2"}); + version2.setFeatures(new String[]{"v2Feature1", "v2Feature2", "v2Feature3"}); ConnectionEnd connectionEnd = new ConnectionEnd(); connectionEnd.setClientId("clientId"); - connectionEnd.setVersions(new Version[] { version1, version2 }); + connectionEnd.setVersions(new Version[]{version1, version2}); connectionEnd.setState(ConnectionEnd.State.STATE_INIT); connectionEnd.setCounterparty(counterparty); connectionEnd.setDelayPeriod(BigInteger.ONE); @@ -109,7 +119,8 @@ public void storeConnectionEnd() { @Test public void storeChannel() { // Arrange - ibc.icon.structs.proto.core.channel.Counterparty counterparty = new ibc.icon.structs.proto.core.channel.Counterparty(); + ibc.icon.structs.proto.core.channel.Counterparty counterparty = + new ibc.icon.structs.proto.core.channel.Counterparty(); counterparty.setPortId("portId"); counterparty.setChannelId("channelId"); @@ -117,7 +128,7 @@ public void storeChannel() { channel.setState(Channel.State.STATE_TRYOPEN); channel.setOrdering(Channel.Order.ORDER_ORDERED); channel.setCounterparty(counterparty); - channel.setConnectionHops(new String[] { "Aerw", "were" }); + channel.setConnectionHops(new String[]{"Aerw", "were"}); channel.setVersion("version"); // Act @@ -133,4 +144,40 @@ public void storeChannel() { assertArrayEquals(channel.getConnectionHops(), storedChannel.getConnectionHops()); assertEquals(channel.getVersion(), storedChannel.getVersion()); } + + @Test + public void storePacket() { + // Arrange + Packet packet = new Packet(); + packet.setSequence(BigInteger.ONE); + packet.setSourcePort("sourcePort"); + packet.setSourceChannel("sourceChannel"); + packet.setDestinationPort("destinationPort"); + packet.setDestinationChannel("destinationChannel"); + packet.setData("data"); + + Height timeoutHeight = new Height(); + timeoutHeight.setRevisionNumber(BigInteger.valueOf(2)); + timeoutHeight.setRevisionHeight(BigInteger.valueOf(3)); + + packet.setTimeoutHeight(timeoutHeight); + packet.setTimeoutTimestamp(BigInteger.valueOf(3)); + // Act + dummyScore.invoke(owner, "setPacket", packet); + + // Assert + Packet storedPacket = (Packet) dummyScore.call("getPacket"); + + assertEquals(packet.getSequence(), storedPacket.getSequence()); + assertEquals(packet.getSourcePort(), storedPacket.getSourcePort()); + assertEquals(packet.getSourceChannel(), storedPacket.getSourceChannel()); + assertEquals(packet.getDestinationPort(), storedPacket.getDestinationPort()); + assertEquals(packet.getDestinationChannel(), storedPacket.getDestinationChannel()); + assertEquals(packet.getData(), storedPacket.getData()); + assertEquals(packet.getTimeoutHeight().getRevisionNumber(), + storedPacket.getTimeoutHeight().getRevisionNumber()); + assertEquals(packet.getTimeoutHeight().getRevisionHeight(), + storedPacket.getTimeoutHeight().getRevisionHeight()); + assertEquals(packet.getTimeoutTimestamp(), storedPacket.getTimeoutTimestamp()); + } } diff --git a/contracts/javascore/settings.gradle b/contracts/javascore/settings.gradle index f923c3e7c..b9847c992 100644 --- a/contracts/javascore/settings.gradle +++ b/contracts/javascore/settings.gradle @@ -1,7 +1,8 @@ rootProject.name = 'javascore' -include ( +include( 'score-util', 'lib', + 'test-lib', 'ibc', // 'xcall', // 'lightclients:archway', diff --git a/contracts/javascore/test-lib/build.gradle b/contracts/javascore/test-lib/build.gradle new file mode 100644 index 000000000..db04ca29d --- /dev/null +++ b/contracts/javascore/test-lib/build.gradle @@ -0,0 +1,22 @@ +version = '0.1.0' + +apply plugin: 'java-library' + +optimizedJar.enabled = false + +dependencies { + compileOnly("foundation.icon:javaee-api:$javaeeVersion") + implementation("foundation.icon:javaee-scorex:$scorexVersion") + implementation project(':score-util') + + implementation("org.mockito:mockito-core:$mockitoCoreVersion") + implementation("org.junit.jupiter:junit-jupiter-api:$jupiterApiVersion") + implementation("org.junit.jupiter:junit-jupiter-api:$jupiterApiVersion") + implementation("org.junit.jupiter:junit-jupiter-params:$jupiterParamsVersion") + implementation("org.junit.jupiter:junit-jupiter-engine:$jupiterEngineVersion") + implementation("foundation.icon:javaee-unittest:$javaeeUnittestVersion") +} + +test { + useJUnitPlatform() +} diff --git a/contracts/javascore/test-lib/src/main/java/ibc/icon/test/MockContract.java b/contracts/javascore/test-lib/src/main/java/ibc/icon/test/MockContract.java new file mode 100644 index 000000000..fd6a1f63c --- /dev/null +++ b/contracts/javascore/test-lib/src/main/java/ibc/icon/test/MockContract.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022-2022 Balanced.network. + * + * 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 ibc.icon.test; + +import com.iconloop.score.test.Account; +import com.iconloop.score.test.Score; +import com.iconloop.score.test.ServiceManager; +import org.mockito.Mockito; +import score.Address; + +public class MockContract { + final private static Address SCORE_ZERO = Address.fromString("cx" + "0".repeat(40)); + public final Account account; + public final T mock; + + public MockContract(Class classToMock, ServiceManager sm, Account admin) throws Exception { + mock = Mockito.mock(classToMock); + Score score = sm.deploy(admin, classToMock, SCORE_ZERO); + score.setInstance(mock); + account = score.getAccount(); + } + + public MockContract(Class classToMock, Class mockClass, ServiceManager sm, Account admin) throws Exception { + mock = Mockito.mock(mockClass); + Score score = sm.deploy(admin, classToMock, SCORE_ZERO); + score.setInstance(mock); + account = score.getAccount(); + } + + public Address getAddress() { + return account.getAddress(); + } +} \ No newline at end of file