본문으로 건너뛰기

MPC

MPC 지갑은 "Multi-Party Computation" 지갑의 약자로, 암호화폐 지갑의 보안성과 사용자 편의성을 개선하기 위해 설계된 기술입니다.
키를 분산하여 저장하고 분산된 참여자들이 공동으로 암호화 작업을 수행하는 방식으로 작동합니다.
이는 다음과 같은 장점을 가질 수 있습니다.

  • MPC 지갑에서는 비밀 키를 여러 조각으로 나누어 분산 저장허게 되며, 어느 하나의 키 조각만으로는 원래의 비밀 키를 복구할 수 없습니다. 또한 하나의 키 조각이 유출되거나 손실되어도 복원이 가능하다는 장점이 있습니다. 이로 하여금 지갑에서 단일 키의 유출이나 분실로 인한 전체 자산의 위험을 막을 수 있습니다.

  • 중앙 서버에 의존하지 않기 때문에 해킹이나 내부자 위협에 강합니다. 또한, 분산된 네트워크 상에서 운영되므로 신뢰할 수 있는 단일 지점이 필요하지 않습니다.

  • 니모닉과 같은 복잡한 키 관리 절차를 간소화할 수 있어 사용자 경험을 개선할 수 있습니다.

지갑 생성

클라이언트 측에서 MPC Key 생성 호출에 필요한 전체 흐름을 보여줍니다. 키 생성 요청에 반환되는 MPC Key는 사용자의 지갑에 대하여 트랜잭션을 가능하게 하는 키 조각입니다.

요청에 사용되는 devicePassword는 키 조각 암호화를 위해 사용됩니다. 키 생성/복구 API 호출 성공 응답으로 받은 encpvstr은 암호화된 KeyShare를 의미합니다. encryptDevicePassword는 요청에 사용된 devicePassword를 WaaS에서 암호화되어 반환하게 되며, 분산 키 조각과 함께 쌍을 이루게 됩니다. 클라이언트는 devicePassword의 원문을 알더라도 복호화하는 것은 불가능합니다.

경고

MPC Key 생성/복구 시 반환된 encryptDevicePassword, encpv, wid는 클라이언트 측에서 안전하게 보관되어야 합니다.

Key 복구

KeyShare가 분실, 훼손된 경우 MPC Key를 복구 할 수 있습니다. 복구한 경우에 새로운 지갑이 생성되는 것은 아니며, KeyShare가 분실, 훼손된 경우에는 해당 KeyShare의 사용이 불가능하기에 복구할 필요가 있습니다.

복구 시마다 wid 값은 1씩 증가하며 지갑 조회 API 호출 시에는 최종 wid 값만 조회가 가능합니다.
또한, encpvstr와 encryptDevicePassword는 서로 한 쌍을 이루게 되며, 클라이언트는 wid, encpvstr, encryptDevicePassword를 안전하게 보관해야 합니다.


아래는 지갑 생성/복구 과정의 이해를 돕기 위한 예제 코드입니다.

// mpc.ts - WAAS 지갑 생성/복구 API 사용 예제

import axios from 'axios';
import qs from 'qs';
import { emailLogin } from './login'; // (2)
import { createSecureChannel, encrypt } from './secureChannel'; // (1)

/*
해당 예제는 정상동작하는 상황을 가정하고, 에러 처리를 따로하지 않음
구현시에 에러 및 예외처리 적용 필요
ts를 js로 빌드하여, dist파일을 실행하도록 package.json설정하여 작성된 예제
package.json 에 해당 스크립트 참고
//``` json
"scripts": {
"start": "tsc | node dist/index.js",
},
//```
*/

const WAAS_BASE_URL: string = 'https://dev-api.abcwaas.com';

// 필수 함수 아님. 유틸성 함수
function getBaseURL(): string {
const waas_base_url: string = process.env.WAAS_BASE_URL || WAAS_BASE_URL;
return waas_base_url;
}

type getWalletResult = {
uid: string;
wid: number;
sid: string;
pvencstr: string;
encryptDevicePassword: string;
};

async function getWallet(
email: string,
encryptedDevicePassword: string,
channelID: string,
accesssToken: string,
): Promise<getWalletResult> {
/*
사용자 고유 MPC 지갑을 생성합니다.

사용자는 1개의 고유 지갑을 소유하게 되며, 이미 생성된 지갑이 존재하는 경우, 기존 지갑을 복구합니다.

devicePassword는 생성 혹은 복구되는 지갑의 Key Share의 암호를 의미합니다.

GetWalletResult example :
>>> {
"uid": "d5b440b8-469b-4e16-8978-d78b73a09c4e",
"wid": 6,
"sid": "0xbE616d5b24903efc58149f3c7511FeC2085c176e",
"pvencstr": "0x1234567890abcdef",
"encryptDevicePassword": "sEbZRmmOrvmMI83XugEzEVwRpwkBBCeXb4jMq1f8Wao="
}

Args:
email (str): 사용자 이메일
encrypted_device_password (str): Secure Channel로 암호화된 devicePassword
channel_id (str): 보안 채널 ID.
access_token (str): 지갑 사용자의 JWT Token

Returns:
GetWalletResult: 사용자의 MPC 지갑 정보

Raises:
HTTPError: 지갑 생성 요청이 실패한 경우
*/
try {
const urlStr = `${getBaseURL()}/wapi/v2/mpc/wallets`;
const data = qs.stringify({
email: email,
devicePassword: encryptedDevicePassword,
});

const response = await axios.post(urlStr, data, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Bearer ${accesssToken}`,
'Secure-Channel': channelID,
},
});

const wallet: getWalletResult = response.data;
return wallet;
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(
`fail to getWallet. stataus code: ${
error.status
}, data: ${JSON.stringify(error.response?.data)}`,
);
}

throw new Error(`fail to getWallet`);
}
}

type walletAccount = {
id: string;
sid: string;
ethAddress: string;
icon: string;
name: string;
signer: string;
pubkey: string;
};

type walletInfo = {
_id: string;
uid: string;
wid: number;
email: string;
accounts: walletAccount[];
favorites: string[];
autoconfirms: string[];
twoFactorEnabled: boolean;
twoFactorResetRetryCount: number;
twoFactorRetryFreezeEndTime: number;
twoFactorFreezeEndTime: number;
};

async function getWalletInfo(accessToken: string): Promise<walletInfo> {
/*
사용자 MPC 지갑을 조회합니다.

Args:
access_token (str): 지갑 사용자의 JWT Token

WalletInfo example:
>>> {
"_id": "657bdc790b67b600128a865f",
"uid": "d5b440b8-469b-4e16-8978-d78b73a09c4e",
"wid": 6,
"email": "test_0@myabcwallet.com",
"accounts": [
{
"id": "0",
"sid": "0xbE616d5b24903efc58149f3c7511FeC2085c176e",
"ethAddress": "0xbE616d5b24903efc58149f3c7511FeC2085c176e",
"icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4Hs....",
"name": "Account 1",
"signer": "mpc",
"pubkey": "0x025c5d89f60eba1b8fc5c2bd2fa28eefe48f4c950815acd7...."
}
],
"favorites": [],
"autoconfirms": [],
"twoFactorEnabled": false,
"twoFactorResetRetryCount": 0,
"twoFactorRetryFreezeEndTime": 0,
"twoFactorFreezeEndTime": 0
}
*/

try {
const urlStr = `${getBaseURL()}/wapi/v2/mpc/wallets/info`;
const response = await axios.get(urlStr, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});

const walletInfoRes: walletInfo = response.data;

return walletInfoRes;
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(
`fail to get walletinfo. status code: ${
error.response?.status
}, data: ${JSON.stringify(error.response?.data)}`,
);
}
throw new Error(`fail to get walletinfo.`);
}
}

export async function mpcScenario() {
const email: string = 'email@email.com'; // 사용자 이메일
const password: string = 'password'; // 사용자 비밀번호
const clientID: string = 'client id'; // 발급받은 Client ID
const clientSecret: string = 'client secret'; // 발급받은 Client Secret

// Secure Channel 생성
const secureChannelRes = await createSecureChannel();

// password는 Secure Channel 암호화가 필요합니다.
const encryptedPassword = encrypt(secureChannelRes, password);

// Client ID / Client Secret
const auth = Buffer.from(`${clientID}:${clientSecret}`).toString('base64'); // (3)

// 로그인
const loginResult = await emailLogin(
email,
encryptedPassword,
secureChannelRes.ChannelID,
auth,
);

// 성공시 jwt token 생성됨
console.log(`access token : ${loginResult.accessToken}`);

const devicePassword = 'password'; // (4)
const encryptedDevicePassword = encrypt(secureChannelRes, devicePassword);

const wallet = await getWallet(
email,
encryptedDevicePassword,
secureChannelRes.ChannelID,
loginResult.accessToken,
);
console.log(`wallet uid: ${wallet.uid}`);
console.log(`wallet wid: ${wallet.wid}`);
console.log(`wallet sid: ${wallet.sid}`);

const walletInfo = await getWalletInfo(loginResult.accessToken);
console.log(`wallet uid: ${walletInfo.uid}`);
console.log(`wallet wid: ${walletInfo.wid}`);
console.log(`wallet sid: ${walletInfo.accounts[0].sid}`);
}
  1. 🙋 Getting Started > Secure Channel
  2. 🙋 Getting Started > Login
  3. 🙋 사전에 발급받은 Client ID / Client Secret 이 필요합니다. Client ID 와 Client Secret 을 base64 로 인코딩 해야 합니다.
  4. 🙋 devicePassword 는 키 조각 암호화를 위해 사용됩니다. Secure Channel 암호화가 필요합니다.