-
Notifications
You must be signed in to change notification settings - Fork 3
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
base: main
Are you sure you want to change the base?
Fetch schema #328
Changes from all commits
ee978c0
b100ccc
e01f20e
53ba9ce
4c90a53
38844c1
d18d1e3
33282b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
|
@@ -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. | ||
* @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. | ||
|
@@ -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")) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In principle there are also which would be great to support since the Schema class does as well as far as I understand. The difference is that the Whereas the new (old,legacy) |
||
// 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(); | ||
} | ||
|
||
} |
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()); | ||
} | ||
|
||
|
||
} |
There was a problem hiding this comment.
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.