Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetch schema #328

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

## Unreleased changes
- Added function in `WasmModule` to exctract a `Schema` if embedded.
- Removed unnecessary `amount` parameter from `InvokeInstanceRequest`.
- Added utility functions for converting between `CCDAmount` and `Energy`. Present in utility class `Converter`.
- Fixed a bug in `CustomEvent`. Removed unnecessary `tag` field.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package com.concordium.sdk.cis2;

import com.concordium.sdk.types.LEB128U;
import lombok.*;

import java.io.ByteArrayOutputStream;
import java.math.BigInteger;
import java.nio.ByteBuffer;

/**
* An amount as specified in the CIS2 specification.
* https://proposals.concordium.software/CIS/cis-2.html#tokenamount
* An amount as specified in the <a href="https://proposals.concordium.software/CIS/cis-2.html#tokenamount">CIS2 specification.</a>
* <p>
* It is an unsigned integer where the max value is 2^256 - 1.
*/
Expand All @@ -25,6 +24,7 @@ public class TokenAmount {
private final BigInteger amount;

private TokenAmount(BigInteger value) {
if (value.compareTo(BigInteger.ZERO) < 0) throw new IllegalArgumentException("TokenAmount must be positive");
if (value.compareTo(MAX_VALUE) > 0) throw new IllegalArgumentException("TokenAmount exceeds max value");
this.amount = value;
}
Expand All @@ -34,34 +34,21 @@ public static TokenAmount from(long value) {
}

public static TokenAmount from(String value) {
if (value.startsWith("-")) throw new IllegalArgumentException("TokenAmount must be positive");
return new TokenAmount(new BigInteger(value));
}

/**
* Encode the {@link TokenAmount} in LEB128 unsigned format.
*
* @return the serialized token amount
* @throws RuntimeException if the resulting byte array would exceed 37 bytes.
* @throws IllegalArgumentException if the resulting byte array would exceed 37 bytes.
*/
@SneakyThrows
public byte[] encode() {
if (this.amount.equals(BigInteger.ZERO)) return new byte[]{0};
val bos = new ByteArrayOutputStream();
var value = this.amount;
// Loop until the most significant byte is zero or less
while (value.compareTo(BigInteger.ZERO) > 0) {
// Take the 7 least significant bits of the current value and set the MSB
var currentByte = value.and(BigInteger.valueOf(0x7F)).byteValue();
value = value.shiftRight(7);
if (value.compareTo(BigInteger.ZERO) != 0) {
currentByte |= 0x80; // Set the MSB to 1 to indicate there are more bytes to come
}
bos.write(currentByte);
if (bos.size() > 37)
throw new IllegalArgumentException("Invalid encoding of TokenAmount. Must not exceed 37 byes.");
try {
return LEB128U.encode(this.amount, 37);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid encoding of TokenAmount. Must not exceed 37 byes.", e);
}
return bos.toByteArray();
}

/**
Expand All @@ -70,26 +57,17 @@ public byte[] encode() {
*
* @param buffer the buffer to read from.
* @return the parsed {@link TokenAmount}
* @throws RuntimeException if the encoding is more than 37 bytes.
* @throws IllegalArgumentException if the encoding is more than 37 bytes.
*/
public static TokenAmount decode(ByteBuffer buffer) {
var result = BigInteger.ZERO;
int shift = 0;
int count = 0;
while (true) {
if (count > 37)
throw new IllegalArgumentException("Tried to decode a TokenAmount which consists of more than 37 bytes.");
byte b = buffer.get();
BigInteger byteValue = BigInteger.valueOf(b & 0x7F); // Mask to get 7 least significant bits
result = result.or(byteValue.shiftLeft(shift));
if ((b & 0x80) == 0) {
break; // If MSB is 0, this is the last byte
}
shift += 7;
count++;
try {
val result = LEB128U.decode(buffer, 37);
return new TokenAmount(result);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Tried to decode a TokenAmount consisting of more than 37 bytes.", e);
}
return new TokenAmount(result);
}
}



}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@

import com.concordium.sdk.crypto.SHA256;
import com.concordium.sdk.responses.modulelist.ModuleRef;
import com.concordium.sdk.transactions.Payload;
import com.concordium.sdk.transactions.TransactionType;
import com.concordium.sdk.types.UInt64;
import com.concordium.sdk.types.LEB128U;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import lombok.val;
import org.bouncycastle.util.Strings;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Optional;

/**
* A compiled Smart Contract Module in WASM with source and version.
Expand Down Expand Up @@ -83,6 +86,17 @@ public static WasmModule from(final byte[] bytes) {
return from(moduleBytes, version);
}

/**
* Create {@link WasmModule} from compiled module WASM file. Passed module should have version prefixed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarify that this is the module format produced by cargo-concordium.

* @param path path to the compiled WASM module.
* @return parsed {@link WasmModule}.
* @throws IOException if an I/O exception occurs reading from the provided path.
*/
public static WasmModule from(String path) throws IOException {
val moduleBytes = Files.readAllBytes(Paths.get(path));
return from(moduleBytes);
}

/**
* Get the identifier of the WasmModule.
* The identifier is a SHA256 hash of the raw module bytes.
Expand All @@ -107,4 +121,55 @@ public byte[] getBytes() {
return buffer.array();
}

/**
* Retrieve the {@link Schema} corresponding to the contract, if embedded.
* Behaviour is not specified if the bytes of {@link WasmModule} do not represent a valid concordium wasm module.
* @return {@link Optional} containing the {@link Schema} if found, empty otherwise.
*/
public Optional<Schema> getSchema() {
val moduleSourceBytes = source.getBytes().clone();
val buffer = ByteBuffer.wrap(moduleSourceBytes);

// Skip 4 byte length of WasmModuleSource (UInt32.BYTES) + 4 byte magic number + 4 byte WASM version
buffer.position(buffer.position() + 12);

while (buffer.hasRemaining()) {
// A section is a 1 byte id followed by the length of the section as a LEB128U encoded u32.
byte id = buffer.get();
int remainingSectionLength = LEB128U.decode(buffer, LEB128U.U32_BYTES).intValue();

// Custom sections have id 0 so all other ids are skipped.
if (id != 0) {
buffer.position(buffer.position() + remainingSectionLength);
continue;
}

// Custom sections have a name encoded as a vector i.e. a length followed by the actual bytes
int beforeName = buffer.position();
int nameLength = LEB128U.decode(buffer, LEB128U.U32_BYTES).intValue();
int nameLengthBytes = buffer.position() - beforeName;

byte[] nameBytes = new byte[nameLength];
buffer.get(nameBytes);

String name = Strings.fromByteArray(nameBytes);

// We've incremented the buffer by reading the length of the name and the name.
remainingSectionLength = remainingSectionLength - nameLengthBytes - nameLength;

if (name.equals("concordium-schema")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In principle there are also concordium-schema-v1 and concordium-schema-v2 , see https://docs.rs/axum/latest/axum/struct.Router.html#method.fallback_service https://github.com/Concordium/concordium-base/blob/87a752f185869ea567387baac384e0e5bc997fd9/smart-contracts/wasm-chain-integration/src/utils.rs#L583

which would be great to support since the Schema class does as well as far as I understand.

The difference is that the concordium-schema section contains the schema that is versioned inside.

Whereas the new (old,legacy) -v1 and -v2 sections, the contents is the unversioned schema and the version is in the name (v0 and v1).

// After reading the name, the remaining contents of the custom section is the schema itself.
byte[] schemaBytes = new byte[remainingSectionLength];
buffer.get(schemaBytes);
Schema schema = Schema.from(schemaBytes);
return Optional.of(schema);
}

// Go to the next section if the name didn't match.
buffer.position(buffer.position() + remainingSectionLength);
}

return Optional.empty();
}

}
113 changes: 113 additions & 0 deletions concordium-sdk/src/main/java/com/concordium/sdk/types/LEB128U.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.concordium.sdk.types;

import lombok.*;

import java.io.ByteArrayOutputStream;
import java.math.BigInteger;
import java.nio.ByteBuffer;

/**
* Contains methods to encode/decode <a href="https://en.wikipedia.org/wiki/LEB128">LEB128U</a> amounts.
* <a href="https://webassembly.github.io/spec/core/binary/values.html">Max byte numbers</a>
*/
public class LEB128U {

/**
* LEB128U integer of unbounded size.
*/
public static int UNBOUNDED = -1;

/**
* Max number of bytes in a LEB128U encoded u64. ceil(64/7).
*/
public static int U64_BYTES = 10;

/**
* Max number of bytes in a LEB128U encoded u32. ceil(32/7).
*/
public static int U32_BYTES = 5;

/**
* Deserialize a LEB128U encoded value from the provided buffer.
* Behaves like decode(buffer, LEB128U.UNBOUNDED).
*
* @param buffer the buffer to read from.
* @return {@link BigInteger} representing the encoded value
*/
public static BigInteger decode(ByteBuffer buffer) {
return decode(buffer, UNBOUNDED);
}

/**
* Deserialize a LEB128U encoded value from the provided buffer.
*
* @param buffer the buffer to read from.
* @param maxSize the max amount of bytes to decode.
* @return {@link BigInteger} representing the encoded value
* @throws IllegalArgumentException if more than `maxSize` bytes are decoded.
*/
public static BigInteger decode(ByteBuffer buffer, int maxSize) {
var result = BigInteger.ZERO;
int shift = 0;
int count = 0;
while (true) {
byte b = buffer.get();
BigInteger byteValue = BigInteger.valueOf(b & 0x7F); // Mask to get 7 least significant bits
result = result.or(byteValue.shiftLeft(shift));
if ((b & 0x80) == 0) {
break; // If MSB is 0, this is the last byte
}
shift += 7;
count++;
if (maxSize != UNBOUNDED && count > maxSize) {
throw new IllegalArgumentException("LEB128U encoded integer is larger than provided max size: " + maxSize);
}
}
return result;
}

/**
* Encode the provided {@link BigInteger} in LEB128 unsigned format.
* Behaves like encode(value, LEB128U.UNBOUNDED).
*
* @param value {@link BigInteger} representing the value to encode.
* @return byte array containing the encoded value.
* @throws IllegalArgumentException if value is negative.
*/
public static byte[] encode(BigInteger value) {
return encode(value, UNBOUNDED);
}

/**
* Encode the provided {@link BigInteger} in LEB128 unsigned format.
*
* @param value {@link BigInteger} representing the value to encode.
* @param maxSize the max amount of bytes to decode.
* @return byte array containing the encoded value.
* @throws IllegalArgumentException if more than `maxSize` bytes are encoded or `value` is negative.
*/
public static byte[] encode(BigInteger value, int maxSize) {
if (value.compareTo(BigInteger.ZERO) < 0) {
throw new IllegalArgumentException("Cannot encode negative amount: " + value);
}
if (value.equals(BigInteger.ZERO)) {
return new byte[]{0};
}
val bos = new ByteArrayOutputStream();
var valueToEncode = value;
// Loop until the most significant byte is zero or less
while (valueToEncode.compareTo(BigInteger.ZERO) > 0) {
// Take the 7 least significant bits of the current value and set the MSB
var currentByte = valueToEncode.and(BigInteger.valueOf(0x7F)).byteValue();
valueToEncode = valueToEncode.shiftRight(7);
if (valueToEncode.compareTo(BigInteger.ZERO) != 0) {
currentByte |= 0x80; // Set the MSB to 1 to indicate there are more bytes to come
}
bos.write(currentByte);
if (maxSize != UNBOUNDED && bos.size() > maxSize) {
throw new IllegalArgumentException("BigInteger: " + value + " does not fit within provided max size: " + maxSize);
}
}
return bos.toByteArray();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.concordium.sdk.smartcontract;

import com.concordium.sdk.transactions.ReceiveName;
import com.concordium.sdk.transactions.smartcontracts.Schema;
import com.concordium.sdk.transactions.smartcontracts.WasmModule;
import com.concordium.sdk.transactions.smartcontracts.parameters.AccountAddressParam;
import com.concordium.sdk.types.AccountAddress;
import org.junit.Test;

import java.io.IOException;
import java.util.Optional;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

/**
* Ensures correct retrieval of {@link Schema} from {@link WasmModule} if embedded.
*/
public class GetSchemaTest {

static WasmModule MODULE_WITH_SCHEMA;

static WasmModule MODULE_WITHOUT_SCHEMA;

static {
try {
MODULE_WITH_SCHEMA = WasmModule.from("./src/test/testresources/smartcontractschema/unit-test-with-schema.wasm");
MODULE_WITHOUT_SCHEMA = WasmModule.from("./src/test/testresources/smartcontractschema/unit-test.wasm");
} catch (IOException e) {
throw new RuntimeException(e);
}
}

@Test
public void shouldFindSchema() {
Optional<Schema> optionalSchema = MODULE_WITH_SCHEMA.getSchema();
assert(optionalSchema.isPresent());
Schema schema = optionalSchema.get();
ReceiveName receiveName = ReceiveName.from("java_sdk_schema_unit_test", "account_address_test");
AccountAddress address = AccountAddress.from("3XSLuJcXg6xEua6iBPnWacc3iWh93yEDMCqX8FbE3RDSbEnT9P");
AccountAddressParam accountAddressParam = new AccountAddressParam(schema, receiveName, address);
try {
// Asserts that the extracted Schema is actually a valid Schema.
accountAddressParam.initialize();
} catch (Exception e) {
fail();
}
}

@Test
public void shouldNotFindSchema() {
Optional<Schema> optionalSchema = MODULE_WITHOUT_SCHEMA.getSchema();
assertEquals(optionalSchema, Optional.empty());
}


}
Binary file not shown.
Binary file not shown.
Loading