Skip to content

Commit

Permalink
feat: split associateAccountToChainArgs (#730)
Browse files Browse the repository at this point in the history
This makes it possible to use the high level associateAccountToChainArgs in most cases, but also use the building blocks in edge cases where parts are already done by other parts of the app (e.g. the wrapping of bytes).

fixes [#2484](https://github.com/KILTprotocol/ticket/issues/2484)

---------

Co-authored-by: Antonio <[email protected]>
  • Loading branch information
weichweich and ntn-x2 authored Mar 7, 2023
1 parent fbcbeac commit 6e32aef
Showing 1 changed file with 121 additions and 36 deletions.
157 changes: 121 additions & 36 deletions packages/did/src/DidLinks/AccountLinks.chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
u8aConcatStrict,
u8aToHex,
u8aWrapBytes,
BN,
} from '@polkadot/util'
import { ApiPromise } from '@polkadot/api'

Expand All @@ -26,7 +27,9 @@ import { ConfigService } from '@kiltprotocol/config'
import { EncodedSignature } from '../Did.utils.js'
import { toChain } from '../Did.chain.js'

/// A chain-agnostic address, which can be encoded using any network prefix.
/**
* A chain-agnostic address, which can be encoded using any network prefix.
*/
export type SubstrateAddress = KeyringPair['address']

export type EthereumAddress = HexString
Expand Down Expand Up @@ -124,28 +127,12 @@ function getUnprefixedSignature(
throw new SDKErrors.SignatureUnverifiableError()
}

/**
* Builds the parameters for an extrinsic to link the `account` to the `did` where the fees and deposit are covered by some third account.
* This extrinsic must be authorized using the same full DID.
* Note that in addition to the signing account and DID used here, the submitting account will also be able to dissolve the link via reclaiming its deposit!
*
* @param accountAddress Address of the account to be linked.
* @param did Full DID to be linked.
* @param sign The sign callback that generates the account signature over the encoded (DidAddress, BlockNumber) tuple.
* @param nBlocksValid The link request will be rejected if submitted later than (current block number + nBlocksValid)?
* @returns An array of parameters for `api.tx.didLookup.associateAccount` that must be DID-authorized by the full DID used.
*/
export async function associateAccountToChainArgs(
accountAddress: Address,
async function getLinkingChallengeV1(
did: DidUri,
sign: (encodedLinkingDetails: HexString) => Promise<Uint8Array>,
nBlocksValid = 10
): Promise<AssociateAccountToChainResult> {
validUntil: BN
): Promise<Uint8Array> {
const api = ConfigService.get('api')

const blockNo = await api.query.system.number()
const validTill = blockNo.addn(nBlocksValid)

// Gets the current definition of BlockNumber (second tx argument) from the metadata.
const BlockNumber =
api.tx.didLookup.associateAccount.meta.args[1].type.toString()
Expand All @@ -158,33 +145,63 @@ export async function associateAccountToChainArgs(
).sub as TypeDef[]
)[0].type // get the type of the first key, which is the DidAddress

const encoded = api
.createType(`(${DidAddress}, ${BlockNumber})`, [toChain(did), validTill])
return api
.createType(`(${DidAddress}, ${BlockNumber})`, [toChain(did), validUntil])
.toU8a()
}

const isAccountId32 = decodeAddress(accountAddress).length > 20
const length = stringToU8a(String(encoded.length))
const paddedDetails = u8aToHex(
isAccountId32
? u8aWrapBytes(encoded)
: u8aConcatStrict([U8A_WRAP_ETHEREUM, length, encoded])
function getLinkingChallengeV2(did: DidUri, validUntil: BN): Uint8Array {
return stringToU8a(
`Publicly link the signing address to ${did} before block number ${validUntil}`
)
}

const { signature, type } = getUnprefixedSignature(
paddedDetails,
await sign(paddedDetails),
accountAddress
)
/**
* Generates the challenge that links a DID to an account.
* The account has to sign the challenge, while the DID will sign the extrinsic that contains the challenge and will
* link the account to the DID.
*
* @param did The URI of the DID that that should be linked to an account.
* @param validUntil Last blocknumber that this challenge is valid for.
* @returns The encoded challenge.
*/
export async function getLinkingChallenge(
did: DidUri,
validUntil: BN
): Promise<Uint8Array> {
const api = ConfigService.get('api')
if (isEthereumEnabled(api)) {
return getLinkingChallengeV2(did, validUntil)
}
return getLinkingChallengeV1(did, validUntil)
}

/**
* Generates the arguments for the extrinsic that links an account to a DID.
*
* @param accountAddress Address of the account to be linked.
* @param validUntil Last blocknumber that this challenge is valid for.
* @param signature The signature for the linking challenge.
* @param type The key type used to sign the challenge.
* @returns The arguments for the call that links account and DID.
*/
export async function getLinkingArguments(
accountAddress: Address,
validUntil: BN,
signature: Uint8Array,
type: KeypairType
): Promise<AssociateAccountToChainResult> {
const api = ConfigService.get('api')

const proof = { [type]: signature } as EncodedSignature

if (isEthereumEnabled(api)) {
if (type === 'ethereum') {
const result = [{ Ethereum: [accountAddress, signature] }, validTill]
const result = [{ Ethereum: [accountAddress, signature] }, validUntil]
// Force type cast to enable the old blockchain types to accept the future format
return result as unknown as AssociateAccountToChainResult
}
const result = [{ Dotsama: [accountAddress, proof] }, validTill]
const result = [{ Polkadot: [accountAddress, proof] }, validUntil]
// Force type cast to enable the old blockchain types to accept the future format
return result as unknown as AssociateAccountToChainResult
}
Expand All @@ -194,5 +211,73 @@ export async function associateAccountToChainArgs(
'Ethereum linking is not yet supported by this chain'
)

return [accountAddress, validTill, proof]
return [accountAddress, validUntil, proof]
}

/**
* Identifies the strategy to wrap raw bytes for signing.
*/
export type WrappingStrategy = 'ethereum' | 'polkadot'

/**
* Wraps the provided challenge according to the key type.
*
* Ethereum addresses will cause the challenge to be prefixed with
* `\x19Ethereum Signed Message:\n` and the length of the message.
*
* For all other key types the message will be wrapped in `<Bytes>..</Bytes>`.
*
* @param type The key type that will sign the challenge.
* @param challenge The challenge to proof ownership of both account and DID.
* @returns The wrapped challenge.
*/
export function getWrappedChallenge(
type: WrappingStrategy,
challenge: Uint8Array
): Uint8Array {
if (type === 'ethereum') {
const length = stringToU8a(String(challenge.length))
return u8aConcatStrict([U8A_WRAP_ETHEREUM, length, challenge])
}
return u8aWrapBytes(challenge)
}

/**
* Builds the parameters for an extrinsic to link the `account` to the `did` where the fees and deposit are covered by some third account.
* This extrinsic must be authorized using the same full DID.
* Note that in addition to the signing account and DID used here, the submitting account will also be able to dissolve the link via reclaiming its deposit!
*
* @param accountAddress Address of the account to be linked.
* @param did Full DID to be linked.
* @param sign The sign callback that generates the account signature over the encoded (DidAddress, BlockNumber) tuple.
* @param nBlocksValid The link request will be rejected if submitted later than (current block number + nBlocksValid)?
* @returns An array of parameters for `api.tx.didLookup.associateAccount` that must be DID-authorized by the full DID used.
*/
export async function associateAccountToChainArgs(
accountAddress: Address,
did: DidUri,
sign: (encodedLinkingDetails: HexString) => Promise<Uint8Array>,
nBlocksValid = 10
): Promise<AssociateAccountToChainResult> {
const api = ConfigService.get('api')

const blockNo = await api.query.system.number()
const validTill = blockNo.addn(nBlocksValid)

const challenge = await getLinkingChallenge(did, validTill)

// ethereum addresses are 42 characters long since they are 20 bytes hex encoded strings
// (they start with 0x, 2 characters per byte)
const predictedType = accountAddress.length === 42 ? 'ethereum' : 'polkadot'
const wrappedChallenge = u8aToHex(
getWrappedChallenge(predictedType, challenge)
)

const { signature, type } = getUnprefixedSignature(
wrappedChallenge,
await sign(wrappedChallenge),
accountAddress
)

return getLinkingArguments(accountAddress, validTill, signature, type)
}

0 comments on commit 6e32aef

Please sign in to comment.