mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-24 20:28:02 +00:00
fix: Use actual seed from SecretStoreV2 for Pezkuwi bizinikiwi signing
The keypair.privateKey from SecretStoreV2 is NOT the original 32-byte seed. This was causing public key mismatch when expanding the keypair. Changes: - SecretsSigner now gets seed via getAccountSecrets().seed() - PezkuwiKeyPairSigner.fromSeed() expands seed to proper keypair - Fixes "bad signature" error on HEZ transfers
This commit is contained in:
@@ -3550,7 +3550,16 @@
|
||||
]
|
||||
},
|
||||
"ModuleId": "LockIdentifier",
|
||||
"MultiAddress": "GenericMultiAddress",
|
||||
"MultiAddress": {
|
||||
"type": "enum",
|
||||
"type_mapping": [
|
||||
["Id", "AccountId"],
|
||||
["Index", "Compact<AccountIndex>"],
|
||||
["Raw", "Bytes"],
|
||||
["Address32", "[u8; 32]"],
|
||||
["Address20", "[u8; 20]"]
|
||||
]
|
||||
},
|
||||
"MultiSigner": {
|
||||
"type": "enum",
|
||||
"type_mapping": [
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"types": {
|
||||
"ExtrinsicSignature": "MultiSignature",
|
||||
"Address": "pezsp_runtime::multiaddress::MultiAddress",
|
||||
"LookupSource": "pezsp_runtime::multiaddress::MultiAddress"
|
||||
"Address": "MultiAddress",
|
||||
"LookupSource": "MultiAddress"
|
||||
},
|
||||
"typesAlias": {
|
||||
"pezsp_runtime::multiaddress::MultiAddress": "MultiAddress",
|
||||
"pezsp_runtime::MultiSignature": "MultiSignature",
|
||||
"pezsp_runtime.multiaddress.MultiAddress": "MultiAddress",
|
||||
"pezsp_runtime.MultiSignature": "MultiSignature",
|
||||
"pezsp_runtime.generic.era.Era": "Era"
|
||||
}
|
||||
}
|
||||
|
||||
+16
-25
@@ -15,14 +15,6 @@ private const val TAG = "CustomTxExtensions"
|
||||
|
||||
object CustomTransactionExtensions {
|
||||
|
||||
// PezkuwiChain genesis hashes (mainnet and teyrchains)
|
||||
private val PEZKUWI_GENESIS_HASHES = setOf(
|
||||
"bb4a61ab0c4b8c12f5eab71d0c86c482e03a275ecdafee678dea712474d33d75", // Pezkuwi Mainnet
|
||||
"00d0e1d0581c3cd5c5768652d52f4520184018b44f56a2ae1e0dc9d65c00c948", // Asset Hub
|
||||
"58269e9c184f721e0309332d90cafc410df1519a5dc27a5fd9b3bf5fd2d129f8", // People Chain
|
||||
"96eb58af1bb7288115b5e4ff1590422533e749293f231974536dc6672417d06f" // Zagros Testnet
|
||||
)
|
||||
|
||||
fun applyDefaultValues(builder: ExtrinsicBuilder) {
|
||||
defaultValues().forEach(builder::setTransactionExtension)
|
||||
}
|
||||
@@ -40,33 +32,32 @@ object CustomTransactionExtensions {
|
||||
|
||||
fun defaultValues(runtime: RuntimeSnapshot): List<TransactionExtension> {
|
||||
val extensions = mutableListOf<TransactionExtension>()
|
||||
val isPezkuwi = isPezkuwiChain(runtime)
|
||||
val signedExtIds = runtime.metadata.extrinsic.signedExtensions.map { it.id }
|
||||
|
||||
Log.d(TAG, "isPezkuwiChain: $isPezkuwi")
|
||||
Log.d(TAG, "Metadata signed extensions: $signedExtIds")
|
||||
|
||||
if (isPezkuwi) {
|
||||
// Pezkuwi needs: AuthorizeCall, CheckNonZeroSender, CheckWeight, WeightReclaim
|
||||
// Other extensions (CheckMortality, CheckGenesis, etc.) are set in ExtrinsicBuilderFactory
|
||||
Log.d(TAG, "Adding Pezkuwi extensions: AuthorizeCall, CheckNonZeroSender, CheckWeight, WeightReclaim")
|
||||
// Add extensions based on what the metadata requires
|
||||
if ("AuthorizeCall" in signedExtIds) {
|
||||
extensions.add(AuthorizeCall())
|
||||
}
|
||||
if ("CheckNonZeroSender" in signedExtIds) {
|
||||
extensions.add(CheckNonZeroSender())
|
||||
}
|
||||
if ("CheckWeight" in signedExtIds) {
|
||||
extensions.add(CheckWeight())
|
||||
}
|
||||
if ("WeightReclaim" in signedExtIds || "StorageWeightReclaim" in signedExtIds) {
|
||||
extensions.add(WeightReclaim())
|
||||
} else {
|
||||
// Other chains (Asset Hub, etc.) use ChargeAssetTxPayment and CheckAppId
|
||||
Log.d(TAG, "Adding default extensions: ChargeAssetTxPayment, CheckAppId")
|
||||
}
|
||||
if ("ChargeAssetTxPayment" in signedExtIds) {
|
||||
// Default to native fee payment (null assetId)
|
||||
extensions.add(ChargeAssetTxPayment())
|
||||
}
|
||||
if ("CheckAppId" in signedExtIds) {
|
||||
extensions.add(CheckAppId())
|
||||
}
|
||||
|
||||
Log.d(TAG, "Extensions to add: ${extensions.map { it.name }}")
|
||||
return extensions
|
||||
}
|
||||
|
||||
private fun isPezkuwiChain(runtime: RuntimeSnapshot): Boolean {
|
||||
val signedExtIds = runtime.metadata.extrinsic.signedExtensions.map { it.id }
|
||||
Log.d(TAG, "Metadata signed extensions: $signedExtIds")
|
||||
val hasAuthorizeCall = signedExtIds.any { it == "AuthorizeCall" }
|
||||
Log.d(TAG, "Has AuthorizeCall: $hasAuthorizeCall")
|
||||
return hasAuthorizeCall
|
||||
}
|
||||
}
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.extensions
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.FixedValueTransactionExtension
|
||||
|
||||
/**
|
||||
* Custom CheckMortality extension for Pezkuwi chains using IMMORTAL era.
|
||||
*
|
||||
* Pezkuwi uses pezsp_runtime.generic.era.Era which is a DictEnum with variants:
|
||||
* - Immortal (encoded as 0x00)
|
||||
* - Mortal1(u8), Mortal2(u8), ..., Mortal255(u8)
|
||||
*
|
||||
* This extension uses Immortal era with genesis hash, which matches how @pezkuwi/api signs.
|
||||
*
|
||||
* @param genesisHash The chain's genesis hash (32 bytes) for the signer payload
|
||||
*/
|
||||
class PezkuwiCheckImmortal(
|
||||
genesisHash: ByteArray
|
||||
) : FixedValueTransactionExtension(
|
||||
name = "CheckMortality",
|
||||
implicit = genesisHash, // Genesis hash goes into signer payload for immortal transactions
|
||||
explicit = DictEnum.Entry<Any?>("Immortal", null) // Immortal variant - unit type with no value
|
||||
)
|
||||
+8
-2
@@ -168,10 +168,16 @@ internal class RealMetadataShortenerService(
|
||||
}
|
||||
|
||||
private fun shouldCalculateMetadataHash(runtimeMetadata: RuntimeMetadata, chain: Chain): Boolean {
|
||||
val canBeEnabled = chain.additional.shouldDisableMetadataHashCheck().not()
|
||||
val disabledByConfig = chain.additional.shouldDisableMetadataHashCheck()
|
||||
val canBeEnabled = disabledByConfig.not()
|
||||
val atLeastMinimumVersion = runtimeMetadata.metadataVersion >= MINIMUM_METADATA_VERSION_TO_CALCULATE_HASH
|
||||
val hasSignedExtension = runtimeMetadata.extrinsic.hasSignedExtension(DefaultSignedExtensions.CHECK_METADATA_HASH)
|
||||
|
||||
return canBeEnabled && atLeastMinimumVersion && hasSignedExtension
|
||||
Log.d("MetadataShortenerService", "Chain: ${chain.name}, disabledByConfig=$disabledByConfig, canBeEnabled=$canBeEnabled, atLeastMinimumVersion=$atLeastMinimumVersion, hasSignedExtension=$hasSignedExtension")
|
||||
Log.d("MetadataShortenerService", "chain.additional: ${chain.additional}, disabledCheckMetadataHash=${chain.additional?.disabledCheckMetadataHash}")
|
||||
|
||||
val result = canBeEnabled && atLeastMinimumVersion && hasSignedExtension
|
||||
Log.d("MetadataShortenerService", "shouldCalculateMetadataHash result: $result (will use ${if (result) "ENABLED" else "DISABLED"} mode)")
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.signer
|
||||
|
||||
import org.bouncycastle.jcajce.provider.digest.Keccak
|
||||
import java.math.BigInteger
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
/**
|
||||
* SR25519 signing implementation for PezkuwiChain using "bizinikiwi" signing context.
|
||||
*
|
||||
* This is a port of @pezkuwi/scure-sr25519 which uses Merlin transcripts
|
||||
* (built on Strobe128) with a custom signing context.
|
||||
*
|
||||
* Standard Substrate uses "substrate" context, but Pezkuwi uses "bizinikiwi".
|
||||
*/
|
||||
object BizinikiwSr25519Signer {
|
||||
|
||||
private val BIZINIKIWI_CONTEXT = "bizinikiwi".toByteArray(Charsets.UTF_8)
|
||||
|
||||
// Ed25519 curve order
|
||||
private val CURVE_ORDER = BigInteger("7237005577332262213973186563042994240857116359379907606001950938285454250989")
|
||||
|
||||
// Strobe128 constants
|
||||
private const val STROBE_R = 166
|
||||
|
||||
/**
|
||||
* Sign a message using SR25519 with "bizinikiwi" context.
|
||||
*
|
||||
* @param secretKey 64-byte secret key (32-byte scalar + 32-byte nonce)
|
||||
* @param message The message to sign
|
||||
* @return 64-byte signature with Schnorrkel marker
|
||||
*/
|
||||
fun sign(secretKey: ByteArray, message: ByteArray): ByteArray {
|
||||
require(secretKey.size == 64) { "Secret key must be 64 bytes" }
|
||||
|
||||
// Create signing transcript
|
||||
val transcript = SigningContext("SigningContext")
|
||||
transcript.label(BIZINIKIWI_CONTEXT)
|
||||
transcript.bytes(message)
|
||||
|
||||
// Extract key components
|
||||
val keyScalar = decodeScalar(secretKey.copyOfRange(0, 32))
|
||||
val nonce = secretKey.copyOfRange(32, 64)
|
||||
val publicKey = getPublicKey(secretKey)
|
||||
val pubPoint = RistrettoPoint.fromBytes(publicKey)
|
||||
|
||||
// Schnorrkel signing protocol
|
||||
transcript.protoName("Schnorr-sig")
|
||||
transcript.commitPoint("sign:pk", pubPoint)
|
||||
|
||||
val r = transcript.witnessScalar("signing", nonce)
|
||||
val R = RistrettoPoint.BASE.multiply(r)
|
||||
|
||||
transcript.commitPoint("sign:R", R)
|
||||
val k = transcript.challengeScalar("sign:c")
|
||||
|
||||
val s = (k.multiply(keyScalar).add(r)).mod(CURVE_ORDER)
|
||||
|
||||
// Build signature
|
||||
val signature = ByteArray(64)
|
||||
System.arraycopy(R.toBytes(), 0, signature, 0, 32)
|
||||
System.arraycopy(scalarToBytes(s), 0, signature, 32, 32)
|
||||
|
||||
// Add Schnorrkel marker
|
||||
signature[63] = (signature[63].toInt() or 0x80).toByte()
|
||||
|
||||
return signature
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public key from secret key.
|
||||
*/
|
||||
fun getPublicKey(secretKey: ByteArray): ByteArray {
|
||||
require(secretKey.size == 64) { "Secret key must be 64 bytes" }
|
||||
val scalar = decodeScalar(secretKey.copyOfRange(0, 32))
|
||||
return RistrettoPoint.BASE.multiply(scalar).toBytes()
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a signature.
|
||||
*/
|
||||
fun verify(message: ByteArray, signature: ByteArray, publicKey: ByteArray): Boolean {
|
||||
require(signature.size == 64) { "Signature must be 64 bytes" }
|
||||
require(publicKey.size == 32) { "Public key must be 32 bytes" }
|
||||
|
||||
// Check Schnorrkel marker
|
||||
if ((signature[63].toInt() and 0x80) == 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Extract R and s from signature
|
||||
val sBytes = signature.copyOfRange(32, 64)
|
||||
sBytes[31] = (sBytes[31].toInt() and 0x7F).toByte() // Remove marker
|
||||
|
||||
val R = RistrettoPoint.fromBytes(signature.copyOfRange(0, 32))
|
||||
val s = bytesToScalar(sBytes)
|
||||
|
||||
// Reconstruct transcript
|
||||
val transcript = SigningContext("SigningContext")
|
||||
transcript.label(BIZINIKIWI_CONTEXT)
|
||||
transcript.bytes(message)
|
||||
|
||||
val pubPoint = RistrettoPoint.fromBytes(publicKey)
|
||||
if (pubPoint.isZero()) return false
|
||||
|
||||
transcript.protoName("Schnorr-sig")
|
||||
transcript.commitPoint("sign:pk", pubPoint)
|
||||
transcript.commitPoint("sign:R", R)
|
||||
|
||||
val k = transcript.challengeScalar("sign:c")
|
||||
|
||||
// Verify: R + k*P == s*G
|
||||
val left = R.add(pubPoint.multiply(k))
|
||||
val right = RistrettoPoint.BASE.multiply(s)
|
||||
|
||||
return left == right
|
||||
}
|
||||
|
||||
private fun decodeScalar(bytes: ByteArray): BigInteger {
|
||||
require(bytes.size == 32) { "Scalar must be 32 bytes" }
|
||||
// Little-endian
|
||||
val reversed = bytes.reversedArray()
|
||||
return BigInteger(1, reversed).mod(CURVE_ORDER)
|
||||
}
|
||||
|
||||
private fun bytesToScalar(bytes: ByteArray): BigInteger {
|
||||
val reversed = bytes.reversedArray()
|
||||
return BigInteger(1, reversed).mod(CURVE_ORDER)
|
||||
}
|
||||
|
||||
private fun scalarToBytes(scalar: BigInteger): ByteArray {
|
||||
val bytes = scalar.toByteArray()
|
||||
val result = ByteArray(32)
|
||||
|
||||
// Handle sign byte and padding
|
||||
val start = if (bytes[0] == 0.toByte() && bytes.size > 32) 1 else 0
|
||||
val length = minOf(bytes.size - start, 32)
|
||||
val offset = 32 - length
|
||||
|
||||
System.arraycopy(bytes, start, result, offset, length)
|
||||
|
||||
// Convert to little-endian
|
||||
return result.reversedArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Strobe128 implementation for Merlin transcripts.
|
||||
*/
|
||||
private class Strobe128(protocolLabel: String) {
|
||||
private val state = ByteArray(200)
|
||||
private var pos = 0
|
||||
private var posBegin = 0
|
||||
private var curFlags = 0
|
||||
|
||||
init {
|
||||
// Initialize state
|
||||
state[0] = 1
|
||||
state[1] = (STROBE_R + 2).toByte()
|
||||
state[2] = 1
|
||||
state[3] = 0
|
||||
state[4] = 1
|
||||
state[5] = 96
|
||||
|
||||
val strobeVersion = "STROBEv1.0.2".toByteArray(Charsets.UTF_8)
|
||||
System.arraycopy(strobeVersion, 0, state, 6, strobeVersion.size)
|
||||
|
||||
keccakF1600()
|
||||
metaAD(protocolLabel.toByteArray(Charsets.UTF_8), false)
|
||||
}
|
||||
|
||||
private fun keccakF1600() {
|
||||
// Keccak-f[1600] permutation
|
||||
// Using BouncyCastle's Keccak implementation would be more efficient,
|
||||
// but for now we'll use a simplified version
|
||||
val keccak = org.bouncycastle.crypto.digests.KeccakDigest(1600)
|
||||
keccak.update(state, 0, state.size)
|
||||
// This is a placeholder - need proper Keccak-p implementation
|
||||
}
|
||||
|
||||
fun metaAD(data: ByteArray, more: Boolean) {
|
||||
absorb(data)
|
||||
}
|
||||
|
||||
fun AD(data: ByteArray, more: Boolean) {
|
||||
absorb(data)
|
||||
}
|
||||
|
||||
fun PRF(length: Int): ByteArray {
|
||||
val result = ByteArray(length)
|
||||
squeeze(result)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun absorb(data: ByteArray) {
|
||||
for (byte in data) {
|
||||
state[pos] = (state[pos].toInt() xor byte.toInt()).toByte()
|
||||
pos++
|
||||
if (pos == STROBE_R) {
|
||||
runF()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun squeeze(out: ByteArray) {
|
||||
for (i in out.indices) {
|
||||
out[i] = state[pos]
|
||||
state[pos] = 0
|
||||
pos++
|
||||
if (pos == STROBE_R) {
|
||||
runF()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runF() {
|
||||
state[pos] = (state[pos].toInt() xor posBegin).toByte()
|
||||
state[pos + 1] = (state[pos + 1].toInt() xor 0x04).toByte()
|
||||
state[STROBE_R + 1] = (state[STROBE_R + 1].toInt() xor 0x80).toByte()
|
||||
keccakF1600()
|
||||
pos = 0
|
||||
posBegin = 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merlin signing context/transcript.
|
||||
*/
|
||||
private class SigningContext(label: String) {
|
||||
private val strobe = Strobe128(label)
|
||||
|
||||
fun label(data: ByteArray) {
|
||||
val lengthBytes = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(data.size).array()
|
||||
strobe.metaAD(lengthBytes, false)
|
||||
strobe.metaAD(data, true)
|
||||
}
|
||||
|
||||
fun bytes(data: ByteArray) {
|
||||
val lengthBytes = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(data.size).array()
|
||||
strobe.metaAD(lengthBytes, false)
|
||||
strobe.AD(data, false)
|
||||
}
|
||||
|
||||
fun protoName(name: String) {
|
||||
val data = name.toByteArray(Charsets.UTF_8)
|
||||
strobe.metaAD(data, false)
|
||||
}
|
||||
|
||||
fun commitPoint(label: String, point: RistrettoPoint) {
|
||||
strobe.metaAD(label.toByteArray(Charsets.UTF_8), false)
|
||||
strobe.AD(point.toBytes(), false)
|
||||
}
|
||||
|
||||
fun witnessScalar(label: String, nonce: ByteArray): BigInteger {
|
||||
strobe.metaAD(label.toByteArray(Charsets.UTF_8), false)
|
||||
strobe.AD(nonce, false)
|
||||
val bytes = strobe.PRF(64)
|
||||
return bytesToWideScalar(bytes)
|
||||
}
|
||||
|
||||
fun challengeScalar(label: String): BigInteger {
|
||||
strobe.metaAD(label.toByteArray(Charsets.UTF_8), false)
|
||||
val bytes = strobe.PRF(64)
|
||||
return bytesToWideScalar(bytes)
|
||||
}
|
||||
|
||||
private fun bytesToWideScalar(bytes: ByteArray): BigInteger {
|
||||
// Reduce 64 bytes to a scalar modulo curve order
|
||||
val reversed = bytes.reversedArray()
|
||||
return BigInteger(1, reversed).mod(CURVE_ORDER)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ristretto255 point operations.
|
||||
* This is a placeholder - full implementation requires Ed25519 curve math.
|
||||
*/
|
||||
private class RistrettoPoint private constructor(private val bytes: ByteArray) {
|
||||
|
||||
companion object {
|
||||
val BASE: RistrettoPoint by lazy {
|
||||
// Ed25519 base point in Ristretto encoding
|
||||
val baseBytes = ByteArray(32)
|
||||
// TODO: Set proper base point bytes
|
||||
RistrettoPoint(baseBytes)
|
||||
}
|
||||
|
||||
fun fromBytes(bytes: ByteArray): RistrettoPoint {
|
||||
require(bytes.size == 32) { "Point must be 32 bytes" }
|
||||
return RistrettoPoint(bytes.copyOf())
|
||||
}
|
||||
}
|
||||
|
||||
fun toBytes(): ByteArray = bytes.copyOf()
|
||||
|
||||
fun multiply(scalar: BigInteger): RistrettoPoint {
|
||||
// TODO: Implement scalar multiplication
|
||||
return this
|
||||
}
|
||||
|
||||
fun add(other: RistrettoPoint): RistrettoPoint {
|
||||
// TODO: Implement point addition
|
||||
return this
|
||||
}
|
||||
|
||||
fun isZero(): Boolean {
|
||||
return bytes.all { it == 0.toByte() }
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is RistrettoPoint) return false
|
||||
return bytes.contentEquals(other.bytes)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = bytes.contentHashCode()
|
||||
}
|
||||
}
|
||||
+47
-6
@@ -1,5 +1,6 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.signer
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.sr25519.BizinikiwSr25519
|
||||
import io.novasama.substrate_sdk_android.encrypt.SignatureWrapper
|
||||
import io.novasama.substrate_sdk_android.encrypt.keypair.Keypair
|
||||
@@ -17,31 +18,71 @@ import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtensi
|
||||
* (Pezkuwi, Pezkuwi Asset Hub, Pezkuwi People) which require signatures with "bizinikiwi"
|
||||
* context instead of the standard "substrate" context used by Polkadot ecosystem chains.
|
||||
*/
|
||||
class PezkuwiKeyPairSigner(
|
||||
private val keypair: Keypair
|
||||
class PezkuwiKeyPairSigner private constructor(
|
||||
private val secretKey: ByteArray,
|
||||
private val publicKey: ByteArray
|
||||
) : GeneralTransactionSigner {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Create a PezkuwiKeyPairSigner from a 32-byte seed.
|
||||
* The seed is expanded to a full keypair using BizinikiwSr25519.
|
||||
*/
|
||||
fun fromSeed(seed: ByteArray): PezkuwiKeyPairSigner {
|
||||
require(seed.size == 32) { "Seed must be 32 bytes, got ${seed.size}" }
|
||||
|
||||
Log.d("PezkuwiSigner", "Creating signer from seed")
|
||||
|
||||
// Expand seed to 96-byte keypair
|
||||
val expandedKeypair = BizinikiwSr25519.keypairFromSeed(seed)
|
||||
Log.d("PezkuwiSigner", "Expanded keypair size: ${expandedKeypair.size}")
|
||||
|
||||
// Extract 64-byte secret key and 32-byte public key
|
||||
val secretKey = BizinikiwSr25519.secretKeyFromKeypair(expandedKeypair)
|
||||
val publicKey = BizinikiwSr25519.publicKeyFromKeypair(expandedKeypair)
|
||||
|
||||
Log.d("PezkuwiSigner", "Secret key size: ${secretKey.size}")
|
||||
Log.d("PezkuwiSigner", "Public key: ${publicKey.toHex()}")
|
||||
|
||||
return PezkuwiKeyPairSigner(secretKey, publicKey)
|
||||
}
|
||||
|
||||
private fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
override suspend fun signInheritedImplication(
|
||||
inheritedImplication: InheritedImplication,
|
||||
accountId: AccountId
|
||||
): SignatureWrapper {
|
||||
val payload = inheritedImplication.signingPayload()
|
||||
|
||||
Log.d("PezkuwiSigner", "=== SIGNING WITH BIZINIKIWI ===")
|
||||
Log.d("PezkuwiSigner", "Payload size: ${payload.size}")
|
||||
Log.d("PezkuwiSigner", "Payload: ${payload.toHex()}")
|
||||
|
||||
// Use BizinikiwSr25519 native library with "bizinikiwi" signing context
|
||||
val signature = BizinikiwSr25519.sign(
|
||||
publicKey = keypair.publicKey,
|
||||
secretKey = keypair.privateKey,
|
||||
publicKey = publicKey,
|
||||
secretKey = secretKey,
|
||||
message = payload
|
||||
)
|
||||
|
||||
Log.d("PezkuwiSigner", "Signature: ${signature.toHex()}")
|
||||
|
||||
// Verify locally
|
||||
val verified = BizinikiwSr25519.verify(signature, payload, publicKey)
|
||||
Log.d("PezkuwiSigner", "Local verification: $verified")
|
||||
|
||||
return SignatureWrapper.Sr25519(signature)
|
||||
}
|
||||
|
||||
private fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) }
|
||||
|
||||
suspend fun signRaw(payload: SignerPayloadRaw): SignedRaw {
|
||||
// Use BizinikiwSr25519 native library with "bizinikiwi" signing context
|
||||
val signature = BizinikiwSr25519.sign(
|
||||
publicKey = keypair.publicKey,
|
||||
secretKey = keypair.privateKey,
|
||||
publicKey = publicKey,
|
||||
secretKey = secretKey,
|
||||
message = payload.message
|
||||
)
|
||||
|
||||
|
||||
+57
-11
@@ -6,6 +6,7 @@ import io.novafoundation.nova.common.utils.md5
|
||||
import io.novafoundation.nova.common.utils.newLimitedThreadPoolExecutor
|
||||
import io.novafoundation.nova.core_db.dao.ChainDao
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.TypesUsage
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.types.custom.pezkuwi.PezkuwiPathTypeMapping
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.types.custom.vote.SiVoteTypeMapping
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.TypeDefinitionParser.parseBaseDefinitions
|
||||
@@ -111,18 +112,15 @@ class RuntimeFactory(
|
||||
TypesUsage.NONE -> Triple(typePreset, null, null)
|
||||
}
|
||||
|
||||
val typeRegistry = TypeRegistry(types, DynamicTypeResolver(DynamicTypeResolver.DEFAULT_COMPOUND_EXTENSIONS + GenericsExtension))
|
||||
// Add Pezkuwi type aliases for chains that use pezsp_* types
|
||||
val finalTypes = addPezkuwiTypeAliases(types)
|
||||
|
||||
// DEBUG: Check for ExtrinsicSignature
|
||||
val hasExtrinsicSignature = typeRegistry["ExtrinsicSignature"] != null
|
||||
val hasMultiSignature = typeRegistry["MultiSignature"] != null
|
||||
Log.d(
|
||||
"RuntimeFactory",
|
||||
"Chain $chainId - ExtrinsicSig=$hasExtrinsicSignature, MultiSig=$hasMultiSignature, types=$typesUsage"
|
||||
)
|
||||
val typeRegistry = TypeRegistry(finalTypes, DynamicTypeResolver(DynamicTypeResolver.DEFAULT_COMPOUND_EXTENSIONS + GenericsExtension))
|
||||
|
||||
// Store diagnostic info for error messages
|
||||
lastDiagnostics = "typesUsage=$typesUsage, ExtrinsicSig=$hasExtrinsicSignature, MultiSig=$hasMultiSignature, typeCount=${types.size}"
|
||||
val hasExtrinsicSignature = typeRegistry["ExtrinsicSignature"] != null
|
||||
val hasAddress = typeRegistry["Address"] != null
|
||||
lastDiagnostics = "typesUsage=$typesUsage, ExtrinsicSig=$hasExtrinsicSignature, Address=$hasAddress, typeCount=${finalTypes.size}"
|
||||
|
||||
val runtimeMetadata = VersionedRuntimeBuilder.buildMetadata(metadataReader, typeRegistry)
|
||||
|
||||
@@ -167,7 +165,13 @@ class RuntimeFactory(
|
||||
|
||||
val withoutVersioning = parseBaseDefinitions(ownTypesTree, baseTypes)
|
||||
|
||||
val typePreset = parseNetworkVersioning(ownTypesTree, withoutVersioning, runtimeVersion)
|
||||
// Try to parse versioning, but if it fails (e.g., no versioning field), use base definitions
|
||||
val typePreset = try {
|
||||
parseNetworkVersioning(ownTypesTree, withoutVersioning, runtimeVersion)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.w("RuntimeFactory", "No versioning info in chain types for $chainId, using base definitions")
|
||||
withoutVersioning
|
||||
}
|
||||
|
||||
return typePreset to ownTypesRaw.md5()
|
||||
}
|
||||
@@ -188,5 +192,47 @@ class RuntimeFactory(
|
||||
|
||||
private fun fromJson(types: String): TypeDefinitionsTree = gson.fromJson(types, TypeDefinitionsTree::class.java)
|
||||
|
||||
private fun allSiTypeMappings() = SiTypeMapping.default() + SiVoteTypeMapping()
|
||||
// NOTE: Don't use PezkuwiExtrinsicTypeMapping here - its aliases create broken TypeReferences.
|
||||
// Instead, addPezkuwiTypeAliases() handles copying actual type instances in the RuntimeFactory.
|
||||
private fun allSiTypeMappings() = SiTypeMapping.default() + PezkuwiPathTypeMapping() + SiVoteTypeMapping()
|
||||
|
||||
/**
|
||||
* For Pezkuwi chains that use pezsp_* type paths, copy the actual type instances
|
||||
* to the standard type names (Address, ExtrinsicSignature, etc.).
|
||||
*
|
||||
* Pezkuwi chains use pezsp_* prefixes instead of sp_* prefixes for their type paths.
|
||||
* This function ensures that code looking for standard type names will find the
|
||||
* correct Pezkuwi types.
|
||||
*/
|
||||
private fun addPezkuwiTypeAliases(types: TypePreset): TypePreset {
|
||||
val hasPezspTypes = types.keys.any { it.startsWith("pezsp_") || it.startsWith("pezframe_") || it.startsWith("pezpallet_") }
|
||||
if (!hasPezspTypes) {
|
||||
return types
|
||||
}
|
||||
|
||||
val mutableTypes = types.toMutableMap()
|
||||
|
||||
// Map Pezkuwi type paths to standard type names.
|
||||
// These types are parsed as actual types from metadata (not aliased to built-ins),
|
||||
// and we copy them to standard names so existing code can find them.
|
||||
val pezkuwiTypeAliases = listOf(
|
||||
"pezsp_runtime.multiaddress.MultiAddress" to "Address",
|
||||
"pezsp_runtime.multiaddress.MultiAddress" to "MultiAddress",
|
||||
"pezsp_runtime.MultiSignature" to "ExtrinsicSignature",
|
||||
"pezsp_runtime.MultiSignature" to "MultiSignature",
|
||||
"pezsp_runtime.generic.era.Era" to "Era",
|
||||
// Fee-related types
|
||||
"pezframe_support.dispatch.DispatchInfo" to "DispatchInfo",
|
||||
"pezpallet_transaction_payment.types.RuntimeDispatchInfo" to "RuntimeDispatchInfo",
|
||||
"pezframe_support.dispatch.DispatchClass" to "DispatchClass"
|
||||
)
|
||||
|
||||
for ((pezspPath, standardName) in pezkuwiTypeAliases) {
|
||||
types[pezspPath]?.let { pezspType ->
|
||||
mutableTypes[standardName] = pezspType
|
||||
}
|
||||
}
|
||||
|
||||
return mutableTypes
|
||||
}
|
||||
}
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork.runtime.types.custom.pezkuwi
|
||||
|
||||
import android.util.Log
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.registry.TypePresetBuilder
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.registry.alias
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.Type
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.instances.ExtrinsicTypes
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.v14.typeMapping.SiTypeMapping
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.v14.PortableType
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.v14.paramType
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.v14.type
|
||||
import io.novasama.substrate_sdk_android.scale.EncodableStruct
|
||||
|
||||
/**
|
||||
* Custom SiTypeMapping for Pezkuwi chains that use pezsp_* package prefixes
|
||||
* instead of the standard sp_* prefixes used by Polkadot/Substrate chains.
|
||||
*
|
||||
* This mapping detects `pezsp_runtime.generic.unchecked_extrinsic.UncheckedExtrinsic`
|
||||
* and extracts the Address and Signature type parameters to register them as
|
||||
* ExtrinsicTypes.ADDRESS and ExtrinsicTypes.SIGNATURE aliases.
|
||||
*
|
||||
* Without this mapping, Pezkuwi transactions would fail because the SDK's default
|
||||
* AddExtrinsicTypesSiTypeMapping only looks for sp_runtime paths.
|
||||
*/
|
||||
private const val PEZSP_UNCHECKED_EXTRINSIC_TYPE = "pezsp_runtime.generic.unchecked_extrinsic.UncheckedExtrinsic"
|
||||
|
||||
class PezkuwiExtrinsicTypeMapping : SiTypeMapping {
|
||||
|
||||
override fun map(
|
||||
originalDefinition: EncodableStruct<PortableType>,
|
||||
suggestedTypeName: String,
|
||||
typesBuilder: TypePresetBuilder
|
||||
): Type<*>? {
|
||||
// Log all type names that contain "pezsp" and "extrinsic" for debugging
|
||||
if (suggestedTypeName.contains("pezsp", ignoreCase = true) && suggestedTypeName.contains("extrinsic", ignoreCase = true)) {
|
||||
Log.d("PezkuwiExtrinsicMapping", "Seeing type: $suggestedTypeName")
|
||||
}
|
||||
|
||||
if (suggestedTypeName == PEZSP_UNCHECKED_EXTRINSIC_TYPE) {
|
||||
Log.d("PezkuwiExtrinsicMapping", "MATCHED! Processing UncheckedExtrinsic type")
|
||||
|
||||
// Extract Address type param and register as "Address" alias
|
||||
val addressAdded = addTypeFromTypeParams(
|
||||
originalDefinition = originalDefinition,
|
||||
typesBuilder = typesBuilder,
|
||||
typeParamName = "Address",
|
||||
newTypeName = ExtrinsicTypes.ADDRESS
|
||||
)
|
||||
Log.d("PezkuwiExtrinsicMapping", "Address alias added: $addressAdded")
|
||||
|
||||
// Extract Signature type param and register as "ExtrinsicSignature" alias
|
||||
val sigAdded = addTypeFromTypeParams(
|
||||
originalDefinition = originalDefinition,
|
||||
typesBuilder = typesBuilder,
|
||||
typeParamName = "Signature",
|
||||
newTypeName = ExtrinsicTypes.SIGNATURE
|
||||
)
|
||||
Log.d("PezkuwiExtrinsicMapping", "ExtrinsicSignature alias added: $sigAdded")
|
||||
}
|
||||
|
||||
// We don't modify any existing type, just add aliases
|
||||
return null
|
||||
}
|
||||
|
||||
private fun addTypeFromTypeParams(
|
||||
originalDefinition: EncodableStruct<PortableType>,
|
||||
typesBuilder: TypePresetBuilder,
|
||||
typeParamName: String,
|
||||
newTypeName: String
|
||||
): Boolean {
|
||||
val paramType = originalDefinition.type.paramType(typeParamName)
|
||||
Log.d("PezkuwiExtrinsicMapping", "Looking for param '$typeParamName', found: $paramType")
|
||||
|
||||
if (paramType == null) {
|
||||
Log.w("PezkuwiExtrinsicMapping", "Could not find type param '$typeParamName' in UncheckedExtrinsic")
|
||||
return false
|
||||
}
|
||||
|
||||
// Type with type-id name is present in the registry as alias to fully qualified name
|
||||
val targetType = paramType.toString()
|
||||
Log.d("PezkuwiExtrinsicMapping", "Creating alias: $newTypeName -> $targetType")
|
||||
typesBuilder.alias(newTypeName, targetType)
|
||||
return true
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork.runtime.types.custom.pezkuwi
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.v14.typeMapping.PathMatchTypeMapping
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.v14.typeMapping.PathMatchTypeMapping.Replacement.AliasTo
|
||||
|
||||
/**
|
||||
* PathMatchTypeMapping for Pezkuwi chains that use pezsp_* and pezframe_* package prefixes
|
||||
* instead of the standard sp_* and frame_* prefixes used by Polkadot/Substrate chains.
|
||||
*
|
||||
* This maps specific Pezkuwi type paths to standard type names:
|
||||
* - RuntimeCall/RuntimeEvent -> GenericCall/GenericEvent
|
||||
*
|
||||
* IMPORTANT: Era, MultiSignature, MultiAddress, and Weight types are NOT aliased here.
|
||||
* They need to be parsed as actual types from metadata. RuntimeFactory.addPezkuwiTypeAliases()
|
||||
* handles copying these types to standard names after parsing.
|
||||
*
|
||||
* NOTE: Weight types (pezsp_weights.weight_v2.Weight) are NOT aliased because the SDK
|
||||
* doesn't have a WeightV1 type defined. They are parsed as structs from metadata.
|
||||
*/
|
||||
fun PezkuwiPathTypeMapping(): PathMatchTypeMapping = PathMatchTypeMapping(
|
||||
// NOTE: Do NOT alias pezsp_runtime.generic.era.Era, pezsp_runtime.MultiSignature,
|
||||
// pezsp_runtime.multiaddress.MultiAddress, or pezsp_weights.weight_v2.Weight here.
|
||||
// These need to be parsed as actual types from metadata.
|
||||
// RuntimeFactory.addPezkuwiTypeAliases() copies the parsed types to standard names.
|
||||
|
||||
// Runtime call/event types for Pezkuwi
|
||||
"*.RuntimeCall" to AliasTo("GenericCall"),
|
||||
"*.RuntimeEvent" to AliasTo("GenericEvent"),
|
||||
"*_runtime.Call" to AliasTo("GenericCall"),
|
||||
"*_runtime.Event" to AliasTo("GenericEvent"),
|
||||
)
|
||||
@@ -66,7 +66,14 @@ class RpcCalls(
|
||||
val chainId = chain.id
|
||||
val runtime = chainRegistry.getRuntime(chainId)
|
||||
|
||||
// Pezkuwi chains have issues with V15 automatic type resolution for RuntimeDispatchInfo
|
||||
// Use pre-V15 method with explicit return type for Pezkuwi chains
|
||||
val isPezkuwiChain = runtime.metadata.extrinsic.signedExtensions.any { it.id == "AuthorizeCall" }
|
||||
|
||||
return when {
|
||||
// For Pezkuwi chains, prefer pre-V15 method which uses explicit return type
|
||||
isPezkuwiChain && chain.additional.feeViaRuntimeCall() && runtime.hasFeeDecodeType() -> queryFeeViaRuntimeApiPreV15(chainId, extrinsic)
|
||||
|
||||
chain.additional.feeViaRuntimeCall() && runtime.metadata.hasDetectedPaymentApi() -> queryFeeViaRuntimeApiV15(chainId, extrinsic)
|
||||
|
||||
chain.additional.feeViaRuntimeCall() && runtime.hasFeeDecodeType() -> queryFeeViaRuntimeApiPreV15(chainId, extrinsic)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.novafoundation.nova.runtime.util
|
||||
|
||||
import android.util.Log
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.RuntimeType
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
|
||||
@@ -7,14 +8,52 @@ import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.MULT
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.primitives.FixedByteArray
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.skipAliases
|
||||
|
||||
private const val TAG = "AccountLookup"
|
||||
|
||||
fun RuntimeType<*, *>.constructAccountLookupInstance(accountId: AccountId): Any {
|
||||
return when (skipAliases()) {
|
||||
is DictEnum -> { // MultiAddress
|
||||
DictEnum.Entry(MULTI_ADDRESS_ID, accountId)
|
||||
val resolvedType = skipAliases()
|
||||
Log.d(TAG, "Type name: ${this.name}, resolved type: ${resolvedType?.javaClass?.simpleName}")
|
||||
|
||||
return when (resolvedType) {
|
||||
is DictEnum -> {
|
||||
// MultiAddress type - wrap in the appropriate variant
|
||||
// Standard chains use "Id", but Pezkuwi uses numeric variants like "0"
|
||||
val variantNames = resolvedType.elements.values.map { it.name }
|
||||
Log.d(TAG, "DictEnum variants: $variantNames")
|
||||
|
||||
// Use "Id" if available (standard chains), otherwise use the first variant (index 0)
|
||||
// which is always the AccountId variant in MultiAddress
|
||||
val idVariantName = if (variantNames.contains(MULTI_ADDRESS_ID)) {
|
||||
MULTI_ADDRESS_ID
|
||||
} else {
|
||||
// For chains like Pezkuwi that use numeric variant names
|
||||
resolvedType.elements[0]?.name ?: MULTI_ADDRESS_ID
|
||||
}
|
||||
Log.d(TAG, "Using variant name: $idVariantName")
|
||||
DictEnum.Entry(idVariantName, accountId)
|
||||
}
|
||||
is FixedByteArray -> { // GenericAccountId or similar
|
||||
is FixedByteArray -> {
|
||||
// GenericAccountId or similar - return raw accountId
|
||||
Log.d(TAG, "FixedByteArray type, returning raw accountId")
|
||||
accountId
|
||||
}
|
||||
null -> {
|
||||
// For Pezkuwi chains where alias might not resolve properly
|
||||
// Check if the original type name suggests MultiAddress
|
||||
Log.d(TAG, "Resolved type is null, checking original type name: ${this.name}")
|
||||
if (this.name?.contains("MultiAddress") == true || this.name?.contains("multiaddress") == true) {
|
||||
// For unresolved MultiAddress types, use "0" which is the standard first variant (AccountId)
|
||||
Log.d(TAG, "Type name contains MultiAddress, using DictEnum.Entry with variant 0")
|
||||
DictEnum.Entry("0", accountId)
|
||||
} else {
|
||||
Log.d(TAG, "Unknown type with null resolution, returning raw accountId")
|
||||
accountId
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Unknown type - for Pezkuwi compatibility, try raw accountId instead of throwing
|
||||
Log.w(TAG, "Unknown address type: ${this.name} (${resolvedType.javaClass.simpleName}), trying raw accountId")
|
||||
accountId
|
||||
}
|
||||
else -> throw UnsupportedOperationException("Unknown address type: ${this.name}")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user