Skip to content
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

feat: split associateAccountToChainArgs #730

Merged
merged 9 commits into from
Mar 7, 2023
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)
}