Skip to content

Commit

Permalink
Custom signed extensions (#12)
Browse files Browse the repository at this point in the history
* Allow custom signed extensions

* Code style

* Bump versions

* Allow for custom signed extensions
  • Loading branch information
valentunn authored Jan 17, 2022
1 parent ecd986c commit 2cb0439
Show file tree
Hide file tree
Showing 12 changed files with 182 additions and 83 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
buildscript {
ext {
// App version
versionName = '1.2.0'
versionName = '1.3.0'
versionCode = 1

// SDK and tools
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ fun v13Preset(): TypePreset = typePreset {
type(Bytes)
type(BitVec)

type(Extrinsic)
type(Extrinsic.Default)

type(CallBytes) // seems to be unused in runtime
type(EraType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,15 @@ private val SIGNED_MASK = 0b1000_0000.toUByte()
private const val TYPE_ADDRESS = "Address"
private const val TYPE_SIGNATURE = "ExtrinsicSignature"

object Extrinsic :
class Extrinsic(
val signedExtrasType: ExtrinsicPayloadExtras = SignedExtras.default
) :
RuntimeType<Extrinsic.EncodingInstance, Extrinsic.DecodedInstance>("ExtrinsicsDecoder") {

companion object {
val Default = Extrinsic()
}

class EncodingInstance(
val signature: Signature?,
val callRepresentation: CallRepresentation
Expand Down Expand Up @@ -65,7 +71,7 @@ object Extrinsic :
Signature(
accountIdentifier = addressType(runtime).decode(scaleCodecReader, runtime),
signature = signatureType(runtime).decode(scaleCodecReader, runtime),
signedExtras = SignedExtras.decode(scaleCodecReader, runtime)
signedExtras = signedExtrasType.decode(scaleCodecReader, runtime)
)
} else {
null
Expand Down Expand Up @@ -107,7 +113,7 @@ object Extrinsic :

val addressBytes = addressType(runtime).bytes(runtime, signature.accountIdentifier)
val signatureBytes = signatureType(runtime).bytes(runtime, signature.signature)
val signedExtrasBytes = SignedExtras.bytes(runtime, signature.signedExtras)
val signedExtrasBytes = signedExtrasType.bytes(runtime, signature.signedExtras)

addressBytes + signatureBytes + signedExtrasBytes
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,46 @@ import io.emeraldpay.polkaj.scale.ScaleCodecReader
import io.emeraldpay.polkaj.scale.ScaleCodecWriter
import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot
import jp.co.soramitsu.fearless_utils.runtime.definitions.types.Type
import jp.co.soramitsu.fearless_utils.runtime.definitions.types.errors.EncodeDecodeException
import jp.co.soramitsu.fearless_utils.runtime.definitions.types.primitives.Compact
import jp.co.soramitsu.fearless_utils.runtime.definitions.types.primitives.u32

typealias ExtrinsicPayloadExtrasInstance = Map<String, Any?>
object SignedExtras {

private const val _MORTALITY = "CheckMortality"
private const val _NONCE = "CheckNonce"
private const val _TIP = "ChargeTransactionPayment"
const val MORTALITY = "CheckMortality"
const val NONCE = "CheckNonce"
const val TIP = "ChargeTransactionPayment"

object SignedExtras : ExtrinsicPayloadExtras(
name = "SignedExtras",
extrasMapping = mapOf(
_MORTALITY to EraType,
_NONCE to Compact("Compact<Index>"),
_TIP to Compact("Compact<u32>")
val default = ExtrinsicPayloadExtras(
mapOf(
MORTALITY to EraType,
NONCE to Compact("Compact<Index>"),
TIP to Compact("Compact<u32>")
)
)
) {
const val ERA = _MORTALITY
const val NONCE = _NONCE
const val TIP = _TIP
}

private const val _GENESIS = "CheckGenesis"
private const val _SPEC_VERSION = "CheckSpecVersion"
private const val _TX_VERSION = "CheckTxVersion"
object AdditionalExtras {

const val GENESIS = "CheckGenesis"
const val SPEC_VERSION = "CheckSpecVersion"
const val TX_VERSION = "CheckTxVersion"
const val BLOCK_HASH = SignedExtras.MORTALITY

object AdditionalExtras : ExtrinsicPayloadExtras(
name = "AdditionalExtras",
extrasMapping = mapOf(
_MORTALITY to H256,
_GENESIS to H256,
_SPEC_VERSION to u32,
_TX_VERSION to u32
val default = ExtrinsicPayloadExtras(
mapOf(
BLOCK_HASH to H256,
GENESIS to H256,
SPEC_VERSION to u32,
TX_VERSION to u32
)
)
) {
const val BLOCK_HASH = _MORTALITY
const val GENESIS = _GENESIS
const val SPEC_VERSION = _SPEC_VERSION
const val TX_VERSION = _TX_VERSION
}

open class ExtrinsicPayloadExtras(
name: String,
private val extrasMapping: Map<String, Type<*>>
) : Type<ExtrinsicPayloadExtrasInstance>(name) {
typealias ExtrinsicPayloadExtrasInstance = Map<String, Any?>

class ExtrinsicPayloadExtras(
val extras: Map<String, Type<*>>
) : Type<ExtrinsicPayloadExtrasInstance>("ExtrinsicPayloadExtras") {

override fun decode(
scaleCodecReader: ScaleCodecReader,
Expand All @@ -58,7 +52,7 @@ open class ExtrinsicPayloadExtras(
val enabledSignedExtras = runtime.metadata.extrinsic.signedExtensions

return enabledSignedExtras.associateWith { name ->
extrasMapping[name]?.decode(scaleCodecReader, runtime)
extras[name]?.decode(scaleCodecReader, runtime)
}
}

Expand All @@ -70,7 +64,7 @@ open class ExtrinsicPayloadExtras(
val enabledSignedExtras = runtime.metadata.extrinsic.signedExtensions

return enabledSignedExtras.forEach { name ->
extrasMapping[name]?.encodeUnsafe(scaleCodecWriter, runtime, value[name])
extras[name]?.encodeUnsafe(scaleCodecWriter, runtime, value[name])
}
}

Expand All @@ -79,7 +73,4 @@ open class ExtrinsicPayloadExtras(
override fun isValidInstance(instance: Any?): Boolean {
return instance is Map<*, *> && instance.keys.all { it is String }
}

private fun unknownSignedExtension(name: String): Nothing =
throw EncodeDecodeException("Unknown signed extension: $name")
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import jp.co.soramitsu.fearless_utils.encrypt.keypair.Keypair
import jp.co.soramitsu.fearless_utils.hash.Hasher.blake2b256
import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot
import jp.co.soramitsu.fearless_utils.runtime.definitions.types.RuntimeType
import jp.co.soramitsu.fearless_utils.runtime.definitions.types.Type
import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.AdditionalExtras
import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.Era
import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.Extrinsic
import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.Extrinsic.EncodingInstance.CallRepresentation
import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.ExtrinsicPayloadExtras
import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.ExtrinsicPayloadExtrasInstance
import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall
import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.SignedExtras
Expand All @@ -29,6 +31,8 @@ private val DEFAULT_TIP = BigInteger.ZERO

private const val PAYLOAD_HASH_THRESHOLD = 256

class SignedExtension(val name: String, val type: Type<*>, val value: Any?)

class ExtrinsicBuilder(
val runtime: RuntimeSnapshot,
private val keypair: Keypair,
Expand All @@ -40,11 +44,14 @@ class ExtrinsicBuilder(
private val blockHash: ByteArray = genesisHash,
private val era: Era = Era.Immortal,
private val tip: BigInteger = DEFAULT_TIP,
private val customSignedExtensions: List<SignedExtension> = emptyList(),
private val signatureConstructor: RuntimeType.InstanceConstructor<SignatureWrapper> = SignatureInstanceConstructor
) {

private val calls = mutableListOf<GenericCall.Instance>()

private val extrinsicType = createExtrinsicType()

fun call(
moduleIndex: Int,
callIndex: Int,
Expand Down Expand Up @@ -130,15 +137,15 @@ class ExtrinsicBuilder(
callRepresentation = callRepresentation
)

return Extrinsic.toHex(runtime, extrinsic)
return extrinsicType.toHex(runtime, extrinsic)
}

private fun buildSignature(
callRepresentation: CallRepresentation
): String {
val multiSignature = buildSignatureObject(callRepresentation)

val signatureType = Extrinsic.signatureType(runtime)
val signatureType = extrinsicType.signatureType(runtime)

return signatureType.toHexUntyped(runtime, multiSignature)
}
Expand All @@ -152,11 +159,7 @@ class ExtrinsicBuilder(
}

private fun buildSignatureObject(callRepresentation: CallRepresentation): Any? {
val signedExtrasInstance = mapOf(
SignedExtras.ERA to era,
SignedExtras.NONCE to nonce,
SignedExtras.TIP to tip
)
val signedExtrasInstance = buildSignedExtras()

val additionalExtrasInstance = mapOf(
AdditionalExtras.BLOCK_HASH to blockHash,
Expand All @@ -174,8 +177,8 @@ class ExtrinsicBuilder(
directWrite(callRepresentation.bytes)
}

SignedExtras.encode(this, runtime, signedExtrasInstance)
AdditionalExtras.encode(this, runtime, additionalExtrasInstance)
extrinsicType.signedExtrasType.encode(this, runtime, signedExtrasInstance)
AdditionalExtras.default.encode(this, runtime, additionalExtrasInstance)
}

val messageToSign = if (payloadBytes.size > PAYLOAD_HASH_THRESHOLD) {
Expand All @@ -189,6 +192,17 @@ class ExtrinsicBuilder(
return signatureConstructor.constructInstance(runtime.typeRegistry, signatureWrapper)
}

private fun createExtrinsicType(): Extrinsic {
val customSignedExtensionTypes = customSignedExtensions.associateBy(
keySelector = { it.name },
valueTransform = { it.type }
)

val allSignedExtensions = SignedExtras.default.extras + customSignedExtensionTypes

return Extrinsic(ExtrinsicPayloadExtras(allSignedExtensions))
}

private fun wrapInBatch(useBatchAll: Boolean): GenericCall.Instance {
val batchModule = runtime.metadata.module("Utility")
val batchFunctionName = if (useBatchAll) "batch_all" else "batch"
Expand All @@ -203,11 +217,20 @@ class ExtrinsicBuilder(
)
}

private fun buildSignedExtras(): ExtrinsicPayloadExtrasInstance = mapOf(
SignedExtras.ERA to era,
SignedExtras.TIP to tip,
SignedExtras.NONCE to nonce
)
private fun buildSignedExtras(): ExtrinsicPayloadExtrasInstance {
val default = mapOf(
SignedExtras.MORTALITY to era,
SignedExtras.TIP to tip,
SignedExtras.NONCE to nonce
)

val custom = customSignedExtensions.associateBy(
keySelector = { it.name },
valueTransform = { it.value }
)

return default + custom
}

private fun requireNotMixingBytesAndInstanceCalls() {
require(calls.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import jp.co.soramitsu.fearless_utils.runtime.definitions.dynamic.DynamicTypeRes
import jp.co.soramitsu.fearless_utils.runtime.definitions.dynamic.extentsions.GenericsExtension
import jp.co.soramitsu.fearless_utils.runtime.definitions.registry.TypeRegistry
import jp.co.soramitsu.fearless_utils.runtime.definitions.registry.v13Preset
import jp.co.soramitsu.fearless_utils.runtime.definitions.registry.v14Preset
import jp.co.soramitsu.fearless_utils.runtime.definitions.v14.TypesParserV14
import jp.co.soramitsu.fearless_utils.runtime.metadata.RuntimeMetadataReader
import jp.co.soramitsu.fearless_utils.runtime.metadata.builder.VersionedRuntimeBuilder
import jp.co.soramitsu.fearless_utils.runtime.metadata.v14.RuntimeMetadataSchemaV14

object RealRuntimeProvider {

Expand All @@ -24,6 +27,29 @@ object RealRuntimeProvider {
return RuntimeSnapshot(typeRegistry, metadata)
}

fun buildRuntimeV14(networkName: String): RuntimeSnapshot {
val gson = Gson()

val metadataRaw = buildRawMetadata(networkName)

val metadataTypePreset = TypesParserV14.parse(metadataRaw.metadata[RuntimeMetadataSchemaV14.lookup], v14Preset())

val networkTypesReader = JsonReader(getResourceReader("${networkName}.json"))
val networkTypesTree = gson.fromJson<TypeDefinitionsTree>(networkTypesReader, TypeDefinitionsTree::class.java)

val completeTypes = TypeDefinitionParser.parseBaseDefinitions(networkTypesTree, metadataTypePreset)

val typeRegistry = TypeRegistry(
types = completeTypes,
dynamicTypeResolver = DynamicTypeResolver(
DynamicTypeResolver.DEFAULT_COMPOUND_EXTENSIONS + GenericsExtension
)
)
val metadata = VersionedRuntimeBuilder.buildMetadata(metadataRaw, typeRegistry)

return RuntimeSnapshot(typeRegistry, metadata)
}

fun buildRawMetadata(networkName: String = "kusama") =
getFileContentFromResources("${networkName}_metadata").run {
RuntimeMetadataReader.read(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class ExtrinsicTest {

@Test
fun `should decode transfer extrinsic`() {
val decoded = Extrinsic.fromHex(runtime, inHex)
val decoded = Extrinsic().fromHex(runtime, inHex)

val multiSignature = decoded.signature!!.tryExtractMultiSignature()!!

Expand All @@ -39,7 +39,7 @@ class ExtrinsicTest {
val batch =
"0x01038400fdc41550fb5186d71cae699c31731b3e1baa10680c7bd6b3831a6d222cf4d16800b2b0e48ec54dd07af525e605c2d674ef57eef7d9932c3ad16f68c1e41a18ce579a207aa910b22bcddcf0a2eea96d4617fe618dff95de548bbf53e1773416700815009000100008040000340a806419d5e278172e45cb0e50da1b031795366c99ddfe0a680bd53b142c630f0000c16ff28623040000340a806419d5e278172e45cb0e50da1b031795366c99ddfe0a680bd53b142c630f00106644db8723"

val decoded = Extrinsic.fromHex(runtime, batch)
val decoded = Extrinsic().fromHex(runtime, batch)

assertEquals(16 to 0, decoded.call.function.index)
assertEquals(2, (decoded.call.arguments["calls"] as List<*>).size)
Expand All @@ -65,7 +65,7 @@ class ExtrinsicTest {
val signedExtras = mapOf(
SignedExtras.TIP to 0.toBigInteger(),
SignedExtras.NONCE to 100.toBigInteger(),
SignedExtras.ERA to Era.Mortal(64, 12)
SignedExtras.MORTALITY to Era.Mortal(64, 12)
)

val signature = SignatureInstanceConstructor.constructInstance(
Expand All @@ -82,10 +82,10 @@ class ExtrinsicTest {
signature = signature,
signedExtras = signedExtras
),
call = call
callRepresentation = CallRepresentation.Instance(call)
)

val encoded = Extrinsic.toHex(runtime, extrinsic)
val encoded = Extrinsic().toHex(runtime, extrinsic)

assertEquals(inHex, encoded)
}
Expand Down
Loading

0 comments on commit 2cb0439

Please sign in to comment.