Skip to content
Merged
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 build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ allprojects {
repositories {
mavenLocal()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}

Expand Down
2 changes: 2 additions & 0 deletions crypto/build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
dependencies {
implementation 'com.github.multiformats:java-multibase:1.1.0'
implementation 'org.bouncycastle:bcprov-jdk15on:1.69'
implementation project(":common")
implementation project(":types")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.strategyobject.substrateclient.crypto.ss58;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor(staticName = "from")
@EqualsAndHashCode
@Getter
public class AddressWithPrefix {
private final byte @NonNull [] address;
private final short prefix;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.strategyobject.substrateclient.crypto.ss58;

import com.google.common.base.Preconditions;
import io.ipfs.multibase.Base58;
import lombok.NonNull;
import lombok.val;
import org.bouncycastle.jcajce.provider.digest.Blake2b;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;

public final class SS58Codec {
private static final byte[] PREFIX = "SS58PRE".getBytes(StandardCharsets.US_ASCII);
private static final int CHECKSUM_LEN = 2;
private static final int ADDRESS_LENGTH = 32;

private SS58Codec() {
}

public static AddressWithPrefix decode(@NonNull String encoded) {
Preconditions.checkArgument(encoded.length() > 0);

val data = Base58.decode(encoded);
if (data.length < 2) {
throw new IllegalArgumentException("Incorrect length of data.");
}

int typeLen;
short prefix;

if (data[0] >= 0 && data[0] <= 63) {
typeLen = 1;
prefix = data[0];
} else if (data[0] >= 64) {
byte lower = (byte) (Byte.toUnsignedInt((byte) (data[0] << 2)) | Byte.toUnsignedInt((byte) (data[1] >> 6)));
byte upper = (byte) (data[1] & 0b00111111);

typeLen = 2;
prefix = (short) (Byte.toUnsignedInt(lower) | (upper << 8));
} else {
throw new IllegalArgumentException("Unknown version.");
}

if (data.length != typeLen + ADDRESS_LENGTH + CHECKSUM_LEN) {
throw new IllegalArgumentException("Incorrect length of data.");
}

val checkSumData = new byte[PREFIX.length + typeLen + ADDRESS_LENGTH];
System.arraycopy(PREFIX, 0, checkSumData, 0, PREFIX.length);
System.arraycopy(data, 0, checkSumData, PREFIX.length, typeLen + ADDRESS_LENGTH);
val checksum = new Blake2b.Blake2b512().digest(checkSumData);
if (checksum[0] != data[data.length - CHECKSUM_LEN] || checksum[1] != data[data.length - CHECKSUM_LEN + 1]) {
throw new IllegalArgumentException("Incorrect checksum.");
}

return AddressWithPrefix.from(Arrays.copyOfRange(data, typeLen, typeLen + ADDRESS_LENGTH), prefix);
}

public static String encode(byte @NonNull [] address, short prefix) {
Preconditions.checkArgument(address.length == ADDRESS_LENGTH,
"The length of address must be 32, but was: " + address.length);

val ident = prefix & 0b0011_1111_1111_1111;
Preconditions.checkArgument(ident == prefix,
"The prefix size is restricted by 14 bits.");

byte[] data;
int typeLen;

if (ident <= 63) {
typeLen = 1;
data = new byte[PREFIX.length + typeLen + address.length];
data[PREFIX.length] = (byte) ident;
} else {
typeLen = 2;
data = new byte[PREFIX.length + typeLen + address.length];
val first = (ident & 0b0000_0000_1111_1100) >> 2;
val second = (ident >> 8) | (ident & 0b0000_0000_0000_0011) << 6;
data[PREFIX.length] = (byte) (first | 0b01000000);
data[PREFIX.length + 1] = (byte) second;
}

System.arraycopy(PREFIX, 0, data, 0, PREFIX.length);
System.arraycopy(address, 0, data, PREFIX.length + typeLen, address.length);

val checksum = new Blake2b.Blake2b512().digest(data);
val encodable = new byte[typeLen + address.length + CHECKSUM_LEN];
System.arraycopy(data, PREFIX.length, encodable, 0, typeLen + address.length);
System.arraycopy(checksum, 0, encodable, typeLen + address.length, CHECKSUM_LEN);

return Base58.encode(encodable);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.strategyobject.substrateclient.crypto.ss58;

import com.strategyobject.substrateclient.common.utils.HexConverter;
import lombok.val;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class SS58CodecTests {
@ParameterizedTest
@CsvSource(value = {
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY:0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d:42",
"cEaNSpz4PxFcZ7nT1VEKrKewH67rfx6MfcM6yKojyyPz7qaqp:0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d:64",
"yGHXkYLYqxijLKKfd9Q2CB9shRVu8rPNBS53wvwGTutYg4zTg:0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d:255",
"VByeGLMtP8r8BYQpNX1sb2VtAW8GYCbtFAeXJwsA2ur3MNRdq:0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d:256",
"yNa8JpqfFB3q8A29rCwSgxvdU94ufJw2yKKxDgznS5m1PoFvn:0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d:16383",
"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty:0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48:42"
},
delimiterString = ":")
void encode(String expected, String hex, short prefix) {
val actual = SS58Codec.encode(HexConverter.toBytes(hex), prefix);

assertEquals(expected, actual);
}

@ParameterizedTest
@CsvSource(value = {
"0xd4:42",
"0x:42",
"0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d:16384"
},
delimiterString = ":")
void encodeThrows(String hex, short prefix) {
assertThrows(IllegalArgumentException.class, () -> SS58Codec.encode(HexConverter.toBytes(hex), prefix));
}

@ParameterizedTest
@CsvSource(value = {
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY:0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d:42",
"cEaNSpz4PxFcZ7nT1VEKrKewH67rfx6MfcM6yKojyyPz7qaqp:0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d:64",
"yGHXkYLYqxijLKKfd9Q2CB9shRVu8rPNBS53wvwGTutYg4zTg:0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d:255",
"VByeGLMtP8r8BYQpNX1sb2VtAW8GYCbtFAeXJwsA2ur3MNRdq:0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d:256",
"yNa8JpqfFB3q8A29rCwSgxvdU94ufJw2yKKxDgznS5m1PoFvn:0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d:16383",
"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty:0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48:42"
},
delimiterString = ":")
void decode(String source, String hex, short prefix) {
val actual = SS58Codec.decode(source);

val expected = AddressWithPrefix.from(HexConverter.toBytes(hex), prefix);
assertEquals(expected, actual);
}

@ParameterizedTest
@CsvSource(value = {
"x", // incorrect length (too short)
"yA3vprfzKUKan9P1eXE6iMGCMSMDZEnAtb6wEjTEf86eMi", // incorrect length (last byte is lost)
"SXYSytZ7wxpQHbRo5FzUFA9wjTfWvTQgYzhVEybWRQvBrhwW", // unknown version (first byte is out of range)
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutL8" // incorrect checksum (second to last byte is different)
})
void decodeThrows(String encoded) {
assertThrows(IllegalArgumentException.class, () -> SS58Codec.decode(encoded));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.strategyobject.substrateclient.rpc.core;

import java.util.concurrent.atomic.AtomicReference;

public final class MetadataRegistry {
private static final short DEFAULT_SS58_VERSION = 42;
private static volatile MetadataRegistry instance;
private final AtomicReference<Short> ss58AddressFormat = new AtomicReference<>(DEFAULT_SS58_VERSION); // TODO it should be read from Metadata

private MetadataRegistry() {
}

public static MetadataRegistry getInstance() {
if (instance == null) {
synchronized (MetadataRegistry.class) {
if (instance == null) {
instance = new MetadataRegistry();
}
}
}

return instance;
}

public short getSS58AddressFormat() {
return ss58AddressFormat.get();
}

// TODO it's a workaround to make possible changing network format until metadata is processed
public void setSS58AddressFormat(short value) {
ss58AddressFormat.getAndSet(value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ void getMetadata() throws ExecutionException, InterruptedException, TimeoutExcep
State rpcSection = sectionFactory.create(State.class, wsProvider);

assertDoesNotThrow(() -> {
rpcSection.getMetadata().get(WAIT_TIMEOUT, TimeUnit.SECONDS);
rpcSection.getMetadata().get(WAIT_TIMEOUT * 3, TimeUnit.SECONDS);
});
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.strategyobject.substrateclient.rpc.sections;

import com.strategyobject.substrateclient.common.utils.HexConverter;
import com.strategyobject.substrateclient.rpc.codegen.sections.RpcGeneratedSectionFactory;
import com.strategyobject.substrateclient.rpc.codegen.sections.RpcInterfaceInitializationException;
import com.strategyobject.substrateclient.rpc.core.registries.RpcEncoderRegistry;
import com.strategyobject.substrateclient.rpc.types.AccountId;
import com.strategyobject.substrateclient.tests.containers.SubstrateVersion;
import com.strategyobject.substrateclient.tests.containers.TestSubstrateContainer;
Expand All @@ -18,42 +18,28 @@
import java.util.concurrent.TimeoutException;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;

@Testcontainers
public class SystemTests {
private static final int WAIT_TIMEOUT = 10;
private static final Network network = Network.newNetwork();

@Container
static final TestSubstrateContainer substrate = new TestSubstrateContainer(SubstrateVersion.V3_0_0)
.withNetwork(network);
static final TestSubstrateContainer substrate = new TestSubstrateContainer(SubstrateVersion.V3_0_0).withNetwork(network);

@Test
void accountNextIndex() throws ExecutionException, InterruptedException, TimeoutException, RpcInterfaceInitializationException {
try (WsProvider wsProvider = WsProvider.builder()
.setEndpoint(substrate.getWsAddress())
.disableAutoConnect()
.build()) {
try (WsProvider wsProvider = WsProvider.builder().setEndpoint(substrate.getWsAddress()).disableAutoConnect().build()) {
wsProvider.connect().get(WAIT_TIMEOUT, TimeUnit.SECONDS);

val sectionFactory = new RpcGeneratedSectionFactory();
System rpcSection = sectionFactory.create(System.class, wsProvider);

// TO DO use registered converter
RpcEncoderRegistry encoderRegistry = mock(RpcEncoderRegistry.class);
when(encoderRegistry.resolve(AccountId.class))
.thenReturn((source, encoders) -> "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY");
try (val utils = mockStatic(RpcEncoderRegistry.class)) {
utils.when(RpcEncoderRegistry::getInstance)
.thenReturn(encoderRegistry);
val result = rpcSection.accountNextIndex(AccountId.fromBytes(
new byte[]{
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32
})).get(WAIT_TIMEOUT, TimeUnit.SECONDS);

assertEquals(0, result);
}
val alicePublicKey = HexConverter.toBytes("0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d");
val result = rpcSection.accountNextIndex(AccountId.fromBytes(alicePublicKey))
.get(WAIT_TIMEOUT, TimeUnit.SECONDS);

assertEquals(0, result);
}
}
}
1 change: 1 addition & 0 deletions rpc/rpc-types/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ dependencies {
implementation project(':scale')
implementation project(':rpc:rpc-core')
implementation project(':common')
implementation project(':crypto')
annotationProcessor project(':rpc:rpc-codegen')
annotationProcessor project(':scale:scale-codegen')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.strategyobject.substrateclient.rpc.types;

import com.google.common.base.Preconditions;
import com.strategyobject.substrateclient.crypto.ss58.SS58Codec;
import com.strategyobject.substrateclient.rpc.core.EncoderPair;
import com.strategyobject.substrateclient.rpc.core.MetadataRegistry;
import com.strategyobject.substrateclient.rpc.core.RpcEncoder;
import com.strategyobject.substrateclient.rpc.core.annotations.AutoRegister;

@AutoRegister(types = AccountId.class)
public class AccountIdEncoder implements RpcEncoder<AccountId> {
@Override
public Object encode(AccountId source, EncoderPair<?>... encoders) {
Preconditions.checkArgument(encoders == null || encoders.length == 0);

return SS58Codec.encode(source.getData(), MetadataRegistry.getInstance().getSS58AddressFormat());
}
}