이 가이드에서는 다자간 계산 API를 사용하여 분산 키, 참여자, 정책을 관리하는 방법을 설명합니다. 주요 사용자 여정을 학습하는 프리뷰 고객을 대상으로 합니다.
개요
MPC API는 두 가지 주요 구성요소로 구성됩니다.
- Core MPC는 분산 키, 제안서, 서명 절차의 수명 주기를 관리합니다.
- 정책 엔진은 거버넌스를 시행하여 작업이 실행되기 전에 승인합니다.
API 액세스와 암호화 서명이 상호작용하는 방식에 관한 자세한 내용은 보안 및 승인을 참고하세요.
기본 요건
시작하기 전에 다음 사항을 확인하세요.
- Google Cloud 프로젝트 프로젝트를 만들려면 프로젝트 만들기를 참고하세요.
- Cloud Shell을 사용하지 않는 경우
gcloudCLI가 설치 및 인증되어 있어야 합니다. 필수 API가 사용 설정되어 있어야 합니다(아직 사용 설정되지 않은 경우). 이 가이드에서 사용되는 새로운 MPC API와 동의 키 생성/서명에 사용하는 경우 Cloud Key Management Service가 필요합니다.
gcloud services enable mpc.googleapis.com cloudkms.googleapis.com사용 설정된 API를 보려면 다음 안내를 따르세요.
gcloud services list --enabledPython 3.9 이상 및 튜토리얼 도우미 라이브러리가 설치되어 있어야 합니다.
pip install embit eth-account eth-utils requests(선택사항) 조직에서 Google 계정 없이 외부 ID 공급업체 (예: Okta 또는 Microsoft Entra ID)를 사용하여 인증하는 경우 직원 ID 제휴 (BYOID) 구성을 참고하여 제휴 ID로 애플리케이션 기본 사용자 인증 정보 (ADC)를 구성하세요.
설정
다음 변수를 업데이트하여 이 가이드 전체의 명령어를 채웁니다.
필수 설정
이러한 매개변수는 반드시 맞춤설정해야 합니다. 가이드는 플레이스홀더 값으로 작동하지 않습니다.
| 매개변수 | 값 | 설명 |
|---|---|---|
| 프로젝트 ID | my-project |
프리뷰 온보딩을 위해 제공한 소비자 프로젝트 ID입니다. |
| 도메인 ID | 67317436-0f29-ba9e-9146-d11c0bd8df9b |
인스턴스에 할당된 고유 도메인 ID입니다. |
| 인스턴스 경로 ID | mpc-unit-bob-123 |
인스턴스에 할당된 경로 인스턴스 ID입니다. |
| 위치 | us-central1 |
배포 위치. us-central1여야 합니다(프리뷰에서는 다른 리전이 지원되지 않음). |
튜토리얼 리소스 라벨
이 튜토리얼에서 만든 리소스의 이름입니다. 기본값은 유효하며 설정 과정의 불편을 최소화하기 위해 그대로 유지할 수 있습니다.
| 매개변수 | 값 | 설명 |
|---|---|---|
| Cloud KMS 키링 ID | mpc-assent-keys |
Cloud KMS에 저장된 외부 동의 키의 Cloud Key Management Service KeyRing입니다. |
| 서명자 1 ID | signer-alice |
MPC 서명자 1 |
| 서명자 2 ID | signer-bob |
MPC 서명자 2. |
| 서명자 3 ID | signer-charlie |
MPC 서명자 3. |
| 정책 작성자 1 ID | author-dave |
정책 작성자 1. |
| 정책 작성자 2 ID | author-eve |
정책 작성자 2. |
| 정책 ID | ethereum-transfer-policy |
정책 ID (예: ethereum-transfer-policy 또는 bitcoin-psbt-policy) |
| 키링 ID | my-key-ring |
키링 ID입니다. |
| 키 ID | my-key |
키 ID입니다. |
| 제안 ID | my-proposal |
제안서 ID입니다. |
1단계: Assent 키 생성
MPC API를 사용하려면 다음과 같은 고유한 역할의 키 쌍을 생성해야 합니다. 이는 외부 암호화 동의 키이며 설정 중에 내부적으로 생성되는 분산 MPC 임곗값 키 샤드와는 다릅니다.
- 정책 작성자: 거버넌스 정책을 승인합니다.
- 서명자: 제안서에 동의 (투표/승인)합니다.
A. 참여자 키 (secp256k1)
참여자에 필요한 암호화 키를 생성합니다. 이러한 명령어는 멱등성을 갖습니다.
Cloud KMS
Cloud Key Management Service에서 승인 키를 관리하는 것이 좋습니다. ID에 키링 생성, 키 생성, 서명, 공개 키 검사기 (예: roles/cloudkms.admin 및 roles/cloudkms.signerVerifier) 보기 권한이 있는지 확인합니다. KeyRing가 생성되었는지 확인한 다음 모든 서명자에 대해 secp256k1 키를 생성합니다.
provision_keyring() {
if gcloud kms keyrings describe "mpc-assent-keys" --location="us-central1" > /dev/null 2>&1; then
echo "KeyRing 'mpc-assent-keys' already exists."
else
echo "KeyRing 'mpc-assent-keys' not found. Creating it..."
gcloud kms keyrings create "mpc-assent-keys" --location="us-central1"
fi
}
provision_kms_key() {
local id=$1
if gcloud kms keys describe "${id}-key" --keyring="mpc-assent-keys" --location="us-central1" > /dev/null 2>&1; then
echo "KMS key '${id}-key' already exists."
else
echo "KMS key '${id}-key' not found. Creating it..."
gcloud kms keys create "${id}-key" \
--keyring="mpc-assent-keys" \
--location="us-central1" \
--purpose=asymmetric-signing \
--default-algorithm=ec-sign-secp256k1-sha256 \
--protection-level=hsm
fi
gcloud kms keys versions get-public-key 1 \
--key="${id}-key" \
--keyring="mpc-assent-keys" \
--location="us-central1" \
--output-file="${id}_pub.pem"
echo "Retrieved public key from KMS and saved to ${id}_pub.pem:"
cat "${id}_pub.pem"
}
# Run Setup
provision_keyring
# Generate Keys
for ID in signer-alice signer-bob signer-charlie; do
echo "Generating KMS key for $ID..."
provision_kms_key "$ID"
done
OpenSSL
표준 OpenSSL을 사용하여 각 참여자의 secp256k1 키를 생성할 수 있습니다.
provision_openssl_key() {
local id=$1
if [[ ! -f "${id}_priv.pem" ]]; then
openssl ecparam -name secp256k1 -genkey -noout -out "${id}_priv.pem"
openssl ec -in "${id}_priv.pem" -pubout -out "${id}_pub.pem"
echo "Wrote private key to ${id}_priv.pem and public key to ${id}_pub.pem:"
cat "${id}_pub.pem"
else
echo "Private key file '${id}_priv.pem' already exists. Skipping OpenSSL generation."
echo "Using existing public key from '${id}_pub.pem':"
cat "${id}_pub.pem"
fi
}
for ID in signer-alice signer-bob signer-charlie; do
echo "Generating OpenSSL key for $ID..."
provision_openssl_key "$ID"
done
B. 정책 작성자 키 (secp256k1)
Cloud KMS
마찬가지로 Cloud Key Management Service에서 이러한 동의 키를 관리하는 것이 좋습니다. ID에 키링 생성, 키 생성, 서명, 공개 키 검사기 보기 (예: roles/cloudkms.admin 및 roles/cloudkms.signerVerifier) 권한이 있는지 확인합니다. KeyRing가 생성되었는지 확인한 후 모든 정책 작성자를 위한 secp256k1 키를 생성합니다.
provision_keyring() {
if gcloud kms keyrings describe "mpc-assent-keys" --location="us-central1" > /dev/null 2>&1; then
echo "KeyRing 'mpc-assent-keys' already exists."
else
echo "KeyRing 'mpc-assent-keys' not found. Creating it..."
gcloud kms keyrings create "mpc-assent-keys" --location="us-central1"
fi
}
provision_kms_key() {
local id=$1
if gcloud kms keys describe "${id}-key" --keyring="mpc-assent-keys" --location="us-central1" > /dev/null 2>&1; then
echo "KMS key '${id}-key' already exists."
else
echo "KMS key '${id}-key' not found. Creating it..."
gcloud kms keys create "${id}-key" \
--keyring="mpc-assent-keys" \
--location="us-central1" \
--purpose=asymmetric-signing \
--default-algorithm=ec-sign-secp256k1-sha256 \
--protection-level=hsm
fi
gcloud kms keys versions get-public-key 1 \
--key="${id}-key" \
--keyring="mpc-assent-keys" \
--location="us-central1" \
--output-file="${id}_pub.pem"
echo "Retrieved public key from KMS and saved to ${id}_pub.pem:"
cat "${id}_pub.pem"
}
provision_keyring
for ID in author-dave author-eve; do
echo "Generating KMS key for $ID..."
provision_kms_key "$ID"
done
OpenSSL
정책 작성자는 secp256k1 곡선을 사용하여 거버넌스 정책 및 관리 작업에 서명합니다. 표준 OpenSSL을 사용하여 이러한 키를 생성할 수 있습니다.
provision_openssl_key() {
local id=$1
if [[ ! -f "${id}_priv.pem" ]]; then
openssl ecparam -name secp256k1 -genkey -noout -out "${id}_priv.pem"
openssl ec -in "${id}_priv.pem" -pubout -out "${id}_pub.pem"
echo "Wrote private key to ${id}_priv.pem and public key to ${id}_pub.pem:"
cat "${id}_pub.pem"
else
echo "Private key file '${id}_priv.pem' already exists. Skipping OpenSSL generation."
echo "Using existing public key from '${id}_pub.pem':"
cat "${id}_pub.pem"
fi
}
for ID in author-dave author-eve; do
echo "Generating OpenSSL key for $ID..."
provision_openssl_key "$ID"
done
2단계: 참여자 등록하기
MPC 흐름 (서명자) 또는 정책 흐름 (정책 작성자)에 참여하려면 먼저 참여자로 등록해야 합니다.
등록하려면 관리자와 참여자 간의 대역 외 조정이 필요합니다.
- 엔티티 (서명자 또는 작성자)는 로컬에서 키 쌍을 생성하고 공개 키를 관리자에게 안전하게 전송합니다.
- 관리자 (표준 Google Cloud IAM 권한 보유)가 API를 호출하여 공개 키를 등록합니다.
curl
provision_participant() {
local id=$1
local name=$2
local pubkey_b64
# Read the public key from the file generated in Step 1
echo "Reading public key from ${id}_pub.pem..."
# Public key must be represented as Base64 encoded bytes of the PEM/DER format.
pubkey_b64=$(openssl base64 -A -in "${id}_pub.pem")
# Check existence
if curl -s -f -o /dev/null -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/signers/$id"; then
echo "Participant '$id' already exists. Skipping registration."
else
echo "Participant '$id' not found. Registering..."
curl -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d "{
\"displayName\": \"$name\",
\"activePublicAssentKey\": {
\"publicKey\": \"$pubkey_b64\",
\"algorithm\": \"PUBLIC_KEY_SIGNATURE_ALGORITHM_ECDSA_SECP256K1\"
}
}" \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/signers?participant_id=$id" | jq
fi
}
echo "Registering Signers..."
for ID in signer-alice signer-bob signer-charlie; do
provision_participant "$ID" "Signer $ID"
done
echo "Registering Policy Authors..."
for ID in author-dave author-eve; do
provision_participant "$ID" "Author $ID"
done
3단계: 정책 엔진 구성
트러스트 번들을 설정하고 정책을 만들어 정책 엔진을 구성합니다.
A. 정책 작성자 트러스트 번들 초기화
승인된 정책 작성자를 정의합니다. 인스턴스의 싱글톤 리소스입니다.
curl
# Get current sequence from the API. If null (initial state), fallback to 0.
NEXT_BUNDLE_SEQUENCE=$(curl -s -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policyAuthorTrustBundle" \
| jq -r '.sequenceNumber // 0')
# Calculate higher sequence number.
NEXT_BUNDLE_SEQUENCE=$((NEXT_BUNDLE_SEQUENCE + 1))
curl -X PATCH \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"sequenceNumber": '"$NEXT_BUNDLE_SEQUENCE"',
"authors": [
{"authorId": "'"author-dave"'"},
{"authorId": "'"author-eve"'"}
]
}' \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policyAuthorTrustBundle | jq
B. 정책 만들기 (PENDING_ACTIVE)
정책 ID (ethereum-transfer-policy 또는 bitcoin-psbt-policy)를 정의하고 허용되는 작업을 설명하는 규칙을 만듭니다. 다양한 시나리오를 관리하기 위해 여러 정책을 구성할 수 있지만, 이 가이드에서는 이더리움 또는 비트코인 거래에 대한 정책을 설정하는 방법을 보여줍니다. 기본적으로 정책 엔진은 빠른 실패 (기본적으로 차단)입니다. 트랜잭션을 명시적으로 허용하는 정책이 없으면 트랜잭션이 거부됩니다.
정책을 만들거나 승인하기 전에 작성자는 의도를 나타내는 페이로드에 서명해야 합니다. 다음 도우미 함수를 사용하여 바이너리 직렬화 및 서명을 처리할 수 있습니다.
이더리움
이 규칙은 Alice의 특정 주소로 소액의 이더리움 전송(2,000,000,000Gwei로 표시되는 2ETH 이하)을 허용합니다.
1. 정족수 서명 생성
Cloud KMS 사용:
sign_policy() {
local author_id=$1
local sequence=$2
local cedar_doc=$3
local policy_id=$4
export _HELPER_POLICY_ID="$policy_id"
export _HELPER_SEQUENCE="$sequence"
export _HELPER_CEDAR_DOC="$cedar_doc"
python3 -c "import os; p='my-project'; l='us-central1'; i='mpc-unit-bob-123'; pol=os.environ.get('_HELPER_POLICY_ID'); s=int(os.environ.get('_HELPER_SEQUENCE')); d=os.environ.get('_HELPER_CEDAR_DOC'); n=f'projects/{p}/locations/{l}/instances/{i}/policyEngine/policies/{pol}'; payload=b'action:ACTIVE\n'+n.encode('utf-8')+b'sequence:'+s.to_bytes(8,'little')+b'cedar_document:'+d.encode('utf-8'); open('payload.bin', 'wb').write(payload)"
gcloud kms asymmetric-sign \
--version=1 \
--key="${author_id}-key" \
--keyring="mpc-assent-keys" \
--location="us-central1" \
--digest-algorithm=sha256 \
--input-file=payload.bin \
--signature-file=policy_sig.bin > /dev/null
openssl base64 -A -in policy_sig.bin
}
# Sign as Author 1 (Sequence 0)
QUORUM_SIGNATURE=$(sign_policy "author-dave" 0 'permit(principal == User::"alice", action == Action::"Transfer", resource == Resource::"mpc_operation") when { context.transaction.to_address == "0x1111111111111111111111111111111111111111" && context.transaction.value_gwei <= 2000000000 };' "ethereum-transfer-policy")
OpenSSL 사용:
sign_policy() {
local author_id=$1
local sequence=$2
local cedar_doc=$3
local policy_id=$4
export _HELPER_POLICY_ID="$policy_id"
export _HELPER_SEQUENCE="$sequence"
export _HELPER_CEDAR_DOC="$cedar_doc"
python3 -c "import os; p='my-project'; l='us-central1'; i='mpc-unit-bob-123'; pol=os.environ.get('_HELPER_POLICY_ID'); s=int(os.environ.get('_HELPER_SEQUENCE')); d=os.environ.get('_HELPER_CEDAR_DOC'); n=f'projects/{p}/locations/{l}/instances/{i}/policyEngine/policies/{pol}'; payload=b'action:ACTIVE\n'+n.encode('utf-8')+b'sequence:'+s.to_bytes(8,'little')+b'cedar_document:'+d.encode('utf-8'); open('payload.bin', 'wb').write(payload)"
openssl dgst -sha256 -sign "${author_id}_priv.pem" -out policy_sig.bin payload.bin
openssl base64 -A -in policy_sig.bin
}
# Sign as Author 1 (Sequence 0)
QUORUM_SIGNATURE=$(sign_policy "author-dave" 0 'permit(principal == User::"alice", action == Action::"Transfer", resource == Resource::"mpc_operation") when { context.transaction.to_address == "0x1111111111111111111111111111111111111111" && context.transaction.value_gwei <= 2000000000 };' "ethereum-transfer-policy")
2. CreatePolicy API 호출
# Check existence
if curl -s -f -o /dev/null -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/ethereum-transfer-policy"; then
echo "Policy 'ethereum-transfer-policy' already exists. Skipping creation."
else
echo "Policy 'ethereum-transfer-policy' not found. Creating..."
curl -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"displayName": "Ethereum Transfer Permit Policy",
"rule": {
"cedarDocument": "permit(principal == User::\"alice\", action == Action::\"Transfer\", resource == Resource::\"mpc_operation\") when { context.transaction.to_address == \"0x1111111111111111111111111111111111111111\" && context.transaction.value_gwei <= 2000000000 };"
},
"createAuthorSignature": {
"authorId": "'"author-dave"'",
"signature": "'"$QUORUM_SIGNATURE"'",
"approvalTime": "'"$(date -u +"%Y-%m-%dT%H:%M:%SZ")"'"
}
}' \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies?policy_id=ethereum-transfer-policy" | jq
fi
비트코인
이 규칙은 bitcoin.transaction.v2 스키마 (BIP 174 PSBT)를 사용하는 비트코인 전송을 허용합니다. 여기서 순 아웃바운드 전송 금액은 250,000사토시 (context.transaction.spend_amount_scaled <= 2500000 또는 10-9 고정 소수점 단위로 정규화된 0.0025BTC)를 초과하지 않고 채굴자 수수료는 10,000사토시 (context.transaction.max_fee_atomic <= 100000)를 초과하지 않습니다.
permit(
principal == User::"alice",
action == Action::"Transfer",
resource == Resource::"mpc_operation"
)
when {
context.transaction.schema == "bitcoin.transaction.v2" &&
context.transaction.spend_amount_scaled <= 2500000 &&
context.transaction.max_fee_atomic <= 100000
};
1. 정족수 서명 생성
Cloud KMS 사용:
sign_policy() {
local author_id=$1
local sequence=$2
local cedar_doc=$3
local policy_id=$4
export _HELPER_POLICY_ID="$policy_id"
export _HELPER_SEQUENCE="$sequence"
export _HELPER_CEDAR_DOC="$cedar_doc"
python3 -c "import os; p='my-project'; l='us-central1'; i='mpc-unit-bob-123'; pol=os.environ.get('_HELPER_POLICY_ID'); s=int(os.environ.get('_HELPER_SEQUENCE')); d=os.environ.get('_HELPER_CEDAR_DOC'); n=f'projects/{p}/locations/{l}/instances/{i}/policyEngine/policies/{pol}'; payload=b'action:ACTIVE\n'+n.encode('utf-8')+b'sequence:'+s.to_bytes(8,'little')+b'cedar_document:'+d.encode('utf-8'); open('payload.bin', 'wb').write(payload)"
gcloud kms asymmetric-sign \
--version=1 \
--key="${author_id}-key" \
--keyring="mpc-assent-keys" \
--location="us-central1" \
--digest-algorithm=sha256 \
--input-file=payload.bin \
--signature-file=policy_sig.bin > /dev/null
openssl base64 -A -in policy_sig.bin
}
# Sign as Author 1 (Sequence 0)
QUORUM_SIGNATURE=$(sign_policy "author-dave" 0 'permit(principal == User::"alice", action == Action::"Transfer", resource == Resource::"mpc_operation") when { context.transaction.schema == "bitcoin.transaction.v2" && context.transaction.spend_amount_scaled <= 2500000 && context.transaction.max_fee_atomic <= 100000 };' "bitcoin-psbt-policy")
OpenSSL 사용:
sign_policy() {
local author_id=$1
local sequence=$2
local cedar_doc=$3
local policy_id=$4
export _HELPER_POLICY_ID="$policy_id"
export _HELPER_SEQUENCE="$sequence"
export _HELPER_CEDAR_DOC="$cedar_doc"
python3 -c "import os; p='my-project'; l='us-central1'; i='mpc-unit-bob-123'; pol=os.environ.get('_HELPER_POLICY_ID'); s=int(os.environ.get('_HELPER_SEQUENCE')); d=os.environ.get('_HELPER_CEDAR_DOC'); n=f'projects/{p}/locations/{l}/instances/{i}/policyEngine/policies/{pol}'; payload=b'action:ACTIVE\n'+n.encode('utf-8')+b'sequence:'+s.to_bytes(8,'little')+b'cedar_document:'+d.encode('utf-8'); open('payload.bin', 'wb').write(payload)"
openssl dgst -sha256 -sign "${author_id}_priv.pem" -out policy_sig.bin payload.bin
openssl base64 -A -in policy_sig.bin
}
# Sign as Author 1 (Sequence 0)
QUORUM_SIGNATURE=$(sign_policy "author-dave" 0 'permit(principal == User::"alice", action == Action::"Transfer", resource == Resource::"mpc_operation") when { context.transaction.schema == "bitcoin.transaction.v2" && context.transaction.spend_amount_scaled <= 2500000 && context.transaction.max_fee_atomic <= 100000 };' "bitcoin-psbt-policy")
2. CreatePolicy API 호출
# Check existence
if curl -s -f -o /dev/null -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/bitcoin-psbt-policy"; then
echo "Policy 'bitcoin-psbt-policy' already exists. Skipping creation."
else
echo "Policy 'bitcoin-psbt-policy' not found. Creating..."
curl -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"displayName": "Bitcoin PSBT Permit Policy",
"rule": {
"cedarDocument": "permit(principal == User::\"alice\", action == Action::\"Transfer\", resource == Resource::\"mpc_operation\") when { context.transaction.schema == \"bitcoin.transaction.v2\" && context.transaction.spend_amount_scaled <= 2500000 && context.transaction.max_fee_atomic <= 100000 };"
},
"createAuthorSignature": {
"authorId": "'"author-dave"'",
"signature": "'"$QUORUM_SIGNATURE"'",
"approvalTime": "'"$(date -u +"%Y-%m-%dT%H:%M:%SZ")"'"
}
}' \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies?policy_id=bitcoin-psbt-policy" | jq
fi
C. 정책 승인 (ACTIVE로 전환)
정책 설정에 M-of-N 서명이 필요한 경우 후속 작성자가 AddQuorumSignature를 호출합니다.
서명을 생성하려면 먼저 정책의 현재 시퀀스를 검색해야 합니다.
이더리움
# 1. Get the policy state and sequence
POLICY_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/ethereum-transfer-policy")
POLICY_STATE=$(echo "$POLICY_INFO" | jq -r '.state // "UNKNOWN"')
if [ "$POLICY_STATE" == "PENDING_ACTIVE" ]; then
POLICY_SEQUENCE=$(echo "$POLICY_INFO" | jq -r '.globalSequence // 0')
echo "Policy Sequence: $POLICY_SEQUENCE"
# 2. Sign as Author 2
QUORUM_SIGNATURE_SEQ=$(sign_policy "author-eve" $POLICY_SEQUENCE 'permit(principal == User::"alice", action == Action::"Transfer", resource == Resource::"mpc_operation") when { context.transaction.to_address == "0x1111111111111111111111111111111111111111" && context.transaction.value_gwei <= 2000000000 };' "ethereum-transfer-policy")
# 3. Call AddQuorumSignature
curl -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"name": "projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/ethereum-transfer-policy",
"signature": {
"authorId": "'"author-eve"'",
"signature": "'"$QUORUM_SIGNATURE_SEQ"'",
"approvalTime": "'"$(date -u +"%Y-%m-%dT%H:%M:%SZ")"'"
}
}' \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/ethereum-transfer-policy:addQuorumSignature" | jq
elif [ "$POLICY_STATE" == "ACTIVE" ]; then
echo "Policy 'ethereum-transfer-policy' is already ACTIVE. Skipping signature."
else
echo "Policy 'ethereum-transfer-policy' is in an unknown or unexpected state: $POLICY_STATE"
fi
비트코인
# 1. Get the policy state and sequence
POLICY_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/bitcoin-psbt-policy")
POLICY_STATE=$(echo "$POLICY_INFO" | jq -r '.state // "UNKNOWN"')
if [ "$POLICY_STATE" == "PENDING_ACTIVE" ]; then
POLICY_SEQUENCE=$(echo "$POLICY_INFO" | jq -r '.globalSequence // 0')
echo "Policy Sequence: $POLICY_SEQUENCE"
# 2. Sign as Author 2
QUORUM_SIGNATURE_SEQ=$(sign_policy "author-eve" $POLICY_SEQUENCE 'permit(principal == User::"alice", action == Action::"Transfer", resource == Resource::"mpc_operation") when { context.transaction.schema == "bitcoin.transaction.v2" && context.transaction.spend_amount_scaled <= 2500000 && context.transaction.max_fee_atomic <= 100000 };' "bitcoin-psbt-policy")
# 3. Call AddQuorumSignature
curl -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"name": "projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/bitcoin-psbt-policy",
"signature": {
"authorId": "'"author-eve"'",
"signature": "'"$QUORUM_SIGNATURE_SEQ"'",
"approvalTime": "'"$(date -u +"%Y-%m-%dT%H:%M:%SZ")"'"
}
}' \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/bitcoin-psbt-policy:addQuorumSignature" | jq
elif [ "$POLICY_STATE" == "ACTIVE" ]; then
echo "Policy 'bitcoin-psbt-policy' is already ACTIVE. Skipping signature."
else
echo "Policy 'bitcoin-psbt-policy' is in an unknown or unexpected state: $POLICY_STATE"
fi
4단계: 키 만들기
이제 키링을 만들고 분산 키를 생성합니다.
A. 키링 만들기
MPC 키의 논리적 그룹입니다.
curl
# Check existence
if curl -s -f -o /dev/null -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring"; then
echo "KeyRing 'my-key-ring' already exists. Skipping creation."
else
echo "KeyRing 'my-key-ring' not found. Creating..."
curl -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{}' \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings?key_ring_id=my-key-ring | jq
fi
B. 분산 키 생성 (DKG)
비동기식 절차를 시작하여 참여자 간에 분산 키를 생성합니다.
curl
# Check existence
if curl -s -f -o /dev/null -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key"; then
echo "Key 'my-key' already exists. Skipping generation."
else
echo "Key 'my-key' not found. Generating..."
curl -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"threshold": 2,
"algorithm": "ECDSA_SECP256K1",
"participantShares": [
{"participantId": "projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/signers/signer-alice"},
{"participantId": "projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/signers/signer-bob"},
{"participantId": "projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/signers/signer-charlie"}
]
}' \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys?key_id=my-key | jq
fi
그러면 장기 실행 작업 (LRO)이 반환됩니다. 키 state가 ACTIVE가 될 때까지 기다려야 합니다(DKG 절차가 성공했음을 의미).
🕵️♂️ 키 상태 확인 (폴링)
응답의 state 필드가 ACTIVE가 될 때까지(DKG 절차가 성공했음을 나타냄) 다음 명령어를 주기적으로 실행하여 키 상태를 폴링합니다.
curl
curl -X GET \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key | jq
C. 블록체인 주소 도출 및 UTXO 찾기
분산 키가 ACTIVE가 되면 MPC 키를 사용할 수 있습니다. 이 단계에서는 MPC 키 생성 (0x04 || X || Y)에서 반환된 65바이트 비압축 공개 키에서 블록체인별 주소를 도출하고 거래에 필요한 미사용 자금을 찾습니다.
이더리움
이더리움 주소는 64바이트 비압축 공개 키 포인트 (pub_bytes[1:])의 Keccak-256 해시에서 파생되며 마지막 20바이트를 사용합니다.
주소 파생 스크립트 (derive_address.py)
다음 블록을 실행하여 derive_address.py를 만들고 실행합니다.
cat <<'EOF' > derive_address.py
import base64
from eth_utils import keccak, to_checksum_address
# Insert your base64 public_key from Step 4.B
PUBLIC_KEY_B64 = "your-public-key"
pub_bytes = base64.b64decode(PUBLIC_KEY_B64)
if len(pub_bytes) == 65 and pub_bytes[0] == 4:
raw_addr = "0x" + keccak(pub_bytes[1:])[-20:].hex()
checksummed_addr = to_checksum_address(raw_addr)
print("Ethereum Address:", checksummed_addr)
else:
print(f"Error: Expected 65-byte uncompressed key, got {len(pub_bytes)} bytes.")
EOF
python3 derive_address.py
이더리움 주소에 자금 입금
6단계에서 브로드캐스트하기 전에 파생된 이더리움 주소에 거래 금액과 가스 비용을 충당할 수 있는 충분한 Sepolia 테스트넷 ETH가 있는지 확인하세요. Ethereum Sepolia용 수도꼭지에 새로 파생된 주소를 입력하여 무료 테스트넷 ETH를 요청할 수 있습니다.
비트코인
비트코인 주소는 압축되지 않은 65바이트 secp256k1 EC 공개 키 (0x04 || X || Y)에서 파생됩니다. 동일한 공개 키에서 여러 주소 형식이 지원됩니다.
- 네이티브 SegWit (P2WPKH / BIP 173): 22바이트 위트니스
scriptPubKey(0014<hash160>)가 있는 Bech32 주소 (메인넷의 경우bc1q..., Testnet4의 경우tb1q...). 거래 수수료가 가장 낮은 최신 형식으로 권장됩니다. - 중첩 SegWit (P2SH-P2WPKH / BIP 49): 23바이트
scriptPubKey(a914<hash160>87) 및 22바이트 redeemScript (0014<hash160>)가 있는 래핑된 SegWit 주소 (메인넷의3..., Testnet4의2...)로, 기존 시스템과 호환됩니다. - 기존 (P2PKH/BIP 44): 25바이트
scriptPubKey(76a914<hash160>88ac)가 있는 원래 Pay-to-PubKey-Hash Base58 주소(메인넷의1..., Testnet4의m/n...)입니다.
주소 파생 스크립트 (derive_btc_address.py)
다음 블록을 실행하여 derive_btc_address.py를 만들고 실행합니다.
cat <<'EOF' > derive_btc_address.py
"""Derives Bitcoin addresses (Native SegWit, Nested SegWit, Legacy) from an MPC public key using embit."""
import base64
from embit import ec, script
from embit.networks import NETWORKS
# Insert the Base64-encoded public key from Step 4.B (publicKey field, decodes to 65 raw bytes)
PUBLIC_KEY_B64 = "your-public-key"
raw_bytes = base64.b64decode(PUBLIC_KEY_B64)
if len(raw_bytes) != 65 or raw_bytes[0] != 4:
raise ValueError(f"Expected 65-byte uncompressed public key (0x04...), got {len(raw_bytes)} bytes")
# 1. Parse and compress public key to 33 bytes
pub = ec.PublicKey.parse(raw_bytes)
pub.compressed = True
compressed_pubkey_hex = pub.sec().hex()
print("================================================================")
print(f"Compressed Public Key (33 bytes): {compressed_pubkey_hex}")
print("================================================================")
# 2. Native SegWit (P2WPKH - BIP 173)
p2wpkh = script.p2wpkh(pub)
print("\n[1. Native SegWit (P2WPKH / Bech32 - BIP 173)]")
print(f" ScriptPubKey: {p2wpkh.data.hex()}")
print(f" Testnet4 Address (tb): {p2wpkh.address(NETWORKS['test'])}")
print(f" Mainnet Address (bc): {p2wpkh.address(NETWORKS['main'])}")
# 3. Nested SegWit (P2SH-P2WPKH - BIP 49)
p2sh_p2wpkh = script.p2sh(p2wpkh)
print("\n[2. Nested SegWit (P2SH-P2WPKH / Base58 - BIP 49)]")
print(f" RedeemScript: {p2wpkh.data.hex()}")
print(f" ScriptPubKey: {p2sh_p2wpkh.data.hex()}")
print(f" Testnet4 Address (2): {p2sh_p2wpkh.address(NETWORKS['test'])}")
print(f" Mainnet Address (3): {p2sh_p2wpkh.address(NETWORKS['main'])}")
# 4. Legacy (P2PKH - BIP 44)
p2pkh = script.p2pkh(pub)
print("\n[3. Legacy (P2PKH / Base58 - BIP 44)]")
print(f" ScriptPubKey: {p2pkh.data.hex()}")
print(f" Testnet4 Address (m/n): {p2pkh.address(NETWORKS['test'])}")
print(f" Mainnet Address (1): {p2pkh.address(NETWORKS['main'])}")
EOF
python3 derive_btc_address.py
사용되지 않은 출력 (UTXO) 찾기
계정 기반 블록체인과 달리 비트코인 트랜잭션은 특정 미사용 트랜잭션 출력 (UTXO)을 사용합니다. 5단계에서 트랜잭션을 구성하기 전에 다음을 수행합니다.
- 파생된 Native SegWit 주소에 자금을 입금합니다 (예: 테스트넷 수도꼭지 사용).
- 비트코인 노드 (
bitcoin-cli listunspent 1 9999999 "[\"tb1q...\"]") 또는 블록 탐색기 API를 사용하여 주소의 미사용 UTXO를 쿼리합니다. - 5단계에서 다음 4가지 매개변수를 확인합니다.
txid: 사용되지 않은 출력의 32바이트 트랜잭션 해시입니다.vout: 0부터 시작하는 출력 색인입니다 (예:0또는1).value: 사토시 단위의 미사용 금액입니다 (예:200000사토시 = 0.002 BTC).scriptPubKey: 잠금 스크립트 16진수입니다 (예:0014...).
5단계: MPC 작업 실행
트랜잭션을 평가하고 제안을 만들어 MPC 작업을 실행합니다.
A. 거래 평가 (승인 토큰 가져오기)
MPC 작업을 서명하기 전에 정책에 따라 페이로드를 승인하여 JWS 토큰과 32바이트 서명 다이제스트를 수신합니다.
이더리움
🛑 케이스 A: 거부 (금액이 너무 높음)
3 ETH의 트랜잭션을 승인하려고 시도합니다(2 ETH 한도 초과).
# Payload for 3 ETH (Hex equivalent contains value_gwei > 2B)
RAW_TX_BYTES_HEX="ef808504a817c8008252089411111111111111111111111111111111111111118829a2241af62c00008083aa36a78080"
curl -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"principal": "User::\"alice\"",
"action": "Action::\"Transfer\"",
"resource": "Resource::\"mpc_operation\"",
"schema": "ETHEREUM_TX_V1",
"payload": "'"$RAW_TX_BYTES_HEX"'"
}' \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine:authorizePayload | jq
예상 응답: decision: DENY
✅ 케이스 B: 승인 (허용된 값)
1Gwei (ETH 한도 2개 이내)의 거래를 승인하려고 시도합니다.
# Payload for 1 Gwei
RAW_TX_BYTES_HEX="eb808504a817c800825208941111111111111111111111111111111111111111843b9aca008083aa36a78080"
RESPONSE=$(curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"principal": "User::\"alice\"",
"action": "Action::\"Transfer\"",
"resource": "Resource::\"mpc_operation\"",
"schema": "ETHEREUM_TX_V1",
"payload": "'"$RAW_TX_BYTES_HEX"'"
}' \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine:authorizePayload)
echo "$RESPONSE" | jq
# Automatically capture variables for next steps
DIGEST_HEX=$(echo "$RESPONSE" | jq -r .digest)
POLICY_ENGINE_TOKEN=$(echo "$RESPONSE" | jq -r .token)
# Convert to Base64 (which curl expects for bytes fields)
DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$DIGEST_HEX')).decode('utf-8'))")
POLICY_ENGINE_TOKEN_B64=$(python3 -c "import base64; print(base64.b64encode('$POLICY_ENGINE_TOKEN'.encode('utf-8')).decode('utf-8'))")
예상 응답: decision: PERMIT 및 token
비트코인
비트코인 UTXO 모델에서 트랜잭션은 사용되지 않은 특정 출력을 사용합니다. SegWit (BIP 143)에서 서명 해시를 계산하려면 지출되는 입력의 사토시 금액과 잠금 스크립트를 알아야 합니다.
정책 엔진에는 각 입력에 PSBT_IN_WITNESS_UTXO가 채워진 BIP 174 PSBT v0 형식 (BITCOIN_TX_V2)이 필요합니다.
PSBT 구성 스크립트 (build_psbt.py)
다음 블록을 실행하여 유효한 BIP 174 PSBT v0 페이로드를 조합합니다.
cat <<'EOF' > build_psbt.py
"""Constructs a BIP 174 PSBT v0 payload using embit for Bitcoin policy evaluation."""
from embit import script, transaction
from embit.psbt import PSBT
# 1. Inputs (From Step 4.C)
PREV_TXID_HEX = "8f594f33156993f1c980c2273dcae4a92cf2517fbc29264421395e63a63916d8"
PREV_VOUT = int("1")
INPUT_VALUE_SATS = int("200000") # Total UTXO input value in satoshis
SENDER_SCRIPTPUBKEY_HEX = "0014eced05cd51d964102a20c856b09080ad620e9da2"
# 2. Output Allocations
RECIPIENT_SATS = int("50000") # Recipient payment in satoshis
MINER_FEE_SATS = int("250") # Miner fee in satoshis
RECIPIENT_SCRIPTPUBKEY_HEX = "0014751e76e8199196d454941c45d1b3a323f1433bd6"
# Change Output calculation: InputSats - RecipientSats - MinerFeeSats
CHANGE_SATS = INPUT_VALUE_SATS - RECIPIENT_SATS - MINER_FEE_SATS
if CHANGE_SATS <= 0:
raise ValueError(f"Insufficient input satoshis ({INPUT_VALUE_SATS}) for payment and fee")
# 3. Construct Unsigned Transaction (Version 2, 1 input, 2 outputs, locktime 0)
tx = transaction.Transaction(
version=2,
vin=[transaction.TransactionInput(
txid=bytes.fromhex(PREV_TXID_HEX),
vout=PREV_VOUT,
sequence=0xFFFFFFFF,
)],
vout=[
transaction.TransactionOutput(
value=RECIPIENT_SATS,
script_pubkey=script.Script(bytes.fromhex(RECIPIENT_SCRIPTPUBKEY_HEX)),
),
transaction.TransactionOutput(
value=CHANGE_SATS,
script_pubkey=script.Script(bytes.fromhex(SENDER_SCRIPTPUBKEY_HEX)),
),
],
locktime=0,
)
# 4. Package into BIP 174 PSBT with Input Witness UTXO
psbt = PSBT(tx)
psbt.inputs[0].witness_utxo = transaction.TransactionOutput(
value=INPUT_VALUE_SATS,
script_pubkey=script.Script(bytes.fromhex(SENDER_SCRIPTPUBKEY_HEX)),
)
print(psbt.serialize().hex())
EOF
BTC_PSBT_HEX=$(python3 build_psbt.py)
echo "Generated PSBT Hex (${#BTC_PSBT_HEX} chars):"
echo "$BTC_PSBT_HEX"
🛑 케이스 A: 거부 (수수료 한도 초과)
채굴자 수수료가 정책 한도인 10,000사토시를 초과하는 거래를 승인하려고 시도합니다. 이 예시에서는 build_psbt.py에서 MINER_FEE_SATS = 15000(및 CHANGE_SATS = 135000)을 설정하면 max_fee_atomic = 150000로 확장되어 정책 한도인 100000를 초과하므로 정책 엔진에서 DENY 결정을 트리거하는 다음 페이로드가 생성됩니다.
# Construct a PSBT payload with miner_fee_sats = 15000 -> max_fee_atomic = 150000 (exceeds 100000 limit)
REJECTED_PSBT_HEX="70736274ff0100710200000001d81639a6635e3921442629bc7f51f22ca9e4ca3d27c280c9f1936915334f598f0100000000ffffffff0250c3000000000000160014751e76e8199196d454941c45d1b3a323f1433bd6580f020000000000160014eced05cd51d964102a20c856b09080ad620e9da2000000000001011f400d030000000000160014eced05cd51d964102a20c856b09080ad620e9da2000000"
curl -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"principal": "User::\"alice\"",
"action": "Action::\"Transfer\"",
"resource": "Resource::\"mpc_operation\"",
"schema": "BITCOIN_TX_V2",
"payload": "'"$REJECTED_PSBT_HEX"'"
}' \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine:authorizePayload | jq
예상 응답: decision: DENY
✅ 케이스 B: 승인 (허용된 비트코인 PSBT)
이전에 구성된 유효한 PSBT를 승인하려고 시도합니다 (RECIPIENT_SATS = 50000는 spend_amount_scaled = 500000 <= 2500000에 매핑되고 MINER_FEE_SATS = 250는 max_fee_atomic = 2500 <= 100000에 매핑됨).
RESPONSE=$(curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"principal": "User::\"alice\"",
"action": "Action::\"Transfer\"",
"resource": "Resource::\"mpc_operation\"",
"schema": "BITCOIN_TX_V2",
"payload": "'"$BTC_PSBT_HEX"'"
}' \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine:authorizePayload)
echo "$RESPONSE" | jq
# Automatically capture variables for next steps
DIGEST_HEX=$(echo "$RESPONSE" | jq -r .digest)
POLICY_ENGINE_TOKEN=$(echo "$RESPONSE" | jq -r .token)
# Convert to Base64 (which curl expects for bytes fields)
DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$DIGEST_HEX')).decode('utf-8'))")
POLICY_ENGINE_TOKEN_B64=$(python3 -c "import base64; print(base64.b64encode('$POLICY_ENGINE_TOKEN'.encode('utf-8')).decode('utf-8'))")
예상 응답: JWS token 및 32바이트 BIP 143 sighash digest와 함께 decision: PERMIT.
이는 다음을 반환합니다.
decision:PERMIT또는DENYtoken: JWS 정책 토큰 (규정 준수 증명)입니다.digest: 16진수로 인코딩된 32바이트 서명 다이제스트입니다.evaluatedPolicyIds: 결정에 영향을 미친 평가된 정책입니다.
B. 제안서 만들기 (서명 시작)
서명을 위해 다이제스트 페이로드를 제공하고 5A단계에서 받은 정책 엔진 토큰을 첨부합니다.
curl
# Check existence
if curl -s -f -o /dev/null -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals/my-proposal"; then
echo "Proposal 'my-proposal' already exists. Skipping creation."
else
echo "Proposal 'my-proposal' not found. Creating..."
curl -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"digest": "'"$DIGEST_B64"'",
"keyVersion": "projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/versions/1",
"initiator": "projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/signers/signer-alice",
"policyApproverSignature": "'"$POLICY_ENGINE_TOKEN_B64"'"
}' \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals?proposal_id=my-proposal | jq
fi
그러면 장기 실행 작업 (LRO)이 반환됩니다. 예상되는 제안서 state는 PENDING_ASSENT입니다(참여자가 서명할 준비가 되었음을 나타냄).
🕵️♂️ 제안 상태 확인 (폴링)
다음 명령어를 주기적으로 실행하여 제안서 상태를 폴링합니다.
curl
curl -X GET \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals/my-proposal | jq
C. 참여자 결정 기록 (동의)
참여자는 로컬 비공개 키 (키 공유)로 제안 해시에 서명하고 승인을 전달해야 합니다.
1. 제안서 다이제스트 서명
Cloud KMS
sign_digest() {
local signer_id=$1
local digest_hex=$2
local digest_b64=$(python3 -c "import base64, sys; print(base64.b64encode(bytes.fromhex('$digest_hex')).decode('utf-8'))")
# Direct API calls are used here because the input is already a pre-computed 32-byte digest from the MPC.
# Tools like gcloud assume input is raw data and apply a redundant hashing layer, altering the payload.
# Unlike Policy signings which hash raw administrative text, Proposal signatures must sign this exact 32-byte hash as-is.
curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "Content-Type: application/json" \
-d "{\"digest\": {\"sha256\": \"$digest_b64\"}}" \
https://cloudkms.googleapis.com/v1/projects/my-project/locations/us-central1/keyRings/mpc-assent-keys/cryptoKeys/${signer_id}-key/cryptoKeyVersions/1:asymmetricSign | jq -r .signature
}
SIGNER_2_SIG_B64=$(sign_digest "signer-bob" "$DIGEST_HEX")
SIGNER_3_SIG_B64=$(sign_digest "signer-charlie" "$DIGEST_HEX")
OpenSSL
sign_digest() {
local signer_id=$1
local digest_hex=$2
python3 -c "import sys; sys.stdout.buffer.write(bytes.fromhex('$digest_hex'))" > digest.bin
openssl pkeyutl -sign -inkey "${signer_id}_priv.pem" -in digest.bin -out digest_sig.bin
openssl base64 -A -in digest_sig.bin
}
SIGNER_2_SIG_B64=$(sign_digest "signer-bob" "$DIGEST_HEX")
SIGNER_3_SIG_B64=$(sign_digest "signer-charlie" "$DIGEST_HEX")
2. 결정 기록
curl
# 1. Check state for Signer 2
PROPOSAL_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals/my-proposal")
PROPOSAL_STATE=$(echo "$PROPOSAL_INFO" | jq -r '.state // "UNKNOWN"')
if [ "$PROPOSAL_STATE" == "PENDING_ASSENT" ]; then
echo "Recording assent for Signer 2..."
curl -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"signature": "'"$SIGNER_2_SIG_B64"'",
"decision": "ASSENT",
"participant": "signer-bob"
}' \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals/my-proposal:recordParticipantDecision | jq
elif [ "$PROPOSAL_STATE" == "READY" ] || [ "$PROPOSAL_STATE" == "SUCCEEDED" ]; then
echo "Proposal is in state $PROPOSAL_STATE. Skipping Signer 2."
else
echo "Proposal is in an unknown or unexpected state: $PROPOSAL_STATE"
fi
# 2. Re-check state for Signer 3 (Signer 2 might have triggered a state change)
PROPOSAL_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals/my-proposal")
PROPOSAL_STATE=$(echo "$PROPOSAL_INFO" | jq -r '.state // "UNKNOWN"')
if [ "$PROPOSAL_STATE" == "PENDING_ASSENT" ]; then
echo "Recording assent for Signer 3..."
curl -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"signature": "'"$SIGNER_3_SIG_B64"'",
"decision": "ASSENT",
"participant": "signer-charlie"
}' \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals/my-proposal:recordParticipantDecision | jq
elif [ "$PROPOSAL_STATE" == "READY" ] || [ "$PROPOSAL_STATE" == "SUCCEEDED" ]; then
echo "Proposal is in state $PROPOSAL_STATE. Skipping Signer 3."
else
echo "Proposal is in an unknown or unexpected state: $PROPOSAL_STATE"
fi
D. 최종 서명 가져오기
승인 기준점에 도달하면 제안서 상태를 확인하거나 서명을 가져옵니다.
curl
curl -X GET \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals/my-proposal | jq
제안서가 SUCCEEDED 상태인 경우 반환된 JSON 내의 finalSignature 필드를 확인합니다.
6단계: 블록체인에 트랜잭션 브로드캐스트
제안서가 SUCCEEDED 상태에 도달하고 finalSignature를 가져오면 마지막 단계는 서명된 유선 트랜잭션을 구성하고 네트워크에 브로드캐스트하는 것입니다.
이더리움
복구 식별자 파생 및 브로드캐스트
MPC API는 원시 64바이트 ECDSA 서명(r 및 s 값으로 구성됨)을 반환합니다. 서버는 서명이 표준 low-s 형식 (EIP-2)인지 확인합니다. 하지만 이더리움은 발신자의 주소를 도출하고 서명을 확인하기 위해 복구 식별자 (v)가 필요하며, 이는 표준 64바이트 MPC 서명에서 직접 반환되지 않습니다.
다음 Python 스크립트는 이러한 복잡성을 처리합니다. 예상 발신자 주소를 일치시켜 올바른 복구 비트를 동적으로 결정하고, 유효한 트랜잭션을 구성하고, 이를 네트워크에 브로드캐스트합니다.
방송 스크립트:
터미널에서 다음 복사하여 붙여넣을 수 있는 블록을 실행하여 broadcast_eth.py를 작성하고 실행합니다.
cat <<'EOF' > broadcast_eth.py
"""Broadcasts an Ethereum transaction using a normalized MPC signature."""
import base64
import os
import requests
from eth_account import Account
try:
from eth_account._utils.transactions import encode_transaction, serializable_unsigned_transaction_from_dict
except ImportError:
from eth_account._utils.legacy_transactions import encode_transaction, serializable_unsigned_transaction_from_dict
# ==========================================
# 1. INPUTS
# ==========================================
FINAL_SIGNATURE_B64 = "your-final-signature"
RPC_URL = "https://ethereum-sepolia-rpc.publicnode.com"
EXPECTED_SENDER = "0xyour-mpc-key-address"
if not FINAL_SIGNATURE_B64 or FINAL_SIGNATURE_B64 == "your-final-signature":
raise ValueError("FINAL_SIGNATURE_B64 is not set. Please provide a valid signature.")
# ==========================================
# 2. TRANSACTION PARAMETERS
# ==========================================
CHAIN_ID = 11155111 # Sepolia Testnet
tx_dict = {
"nonce": int("0"),
"gasPrice": int("20000000000"), # 20 Gwei
"gas": int("21000"),
"to": "0x1111111111111111111111111111111111111111",
"value": int("1000000000"), # 1 Gwei
"data": b"",
"chainId": CHAIN_ID,
}
# ==========================================
# 3. DECODE & SPLIT SIGNATURE
# ==========================================
missing_padding = len(FINAL_SIGNATURE_B64) % 4
if missing_padding:
FINAL_SIGNATURE_B64 += "=" * (4 - missing_padding)
sig_bytes = base64.b64decode(FINAL_SIGNATURE_B64)
r = int.from_bytes(sig_bytes[:32], byteorder="big")
s = int.from_bytes(sig_bytes[32:], byteorder="big")
# ==========================================
# 4. RECOVERY IDENTIFIER (V) DERIVATION
# ==========================================
chain_v_base = (CHAIN_ID * 2) + 35
# ==========================================
# 5. GENERATE UNSIGNED TX & RECOVER SENDER ADDRESS
# ==========================================
unsigned_tx = serializable_unsigned_transaction_from_dict(tx_dict)
derived_sender = None
encoded_tx = None
final_v = chain_v_base
EXPECTED_SENDER_PLACEHOLDER = "0xyour-mpc-key-address"
is_valid_expected = EXPECTED_SENDER and EXPECTED_SENDER != EXPECTED_SENDER_PLACEHOLDER
for parity in [0, 1]:
try_v = chain_v_base + parity
try:
tx_candidate = encode_transaction(unsigned_tx, vrs=(try_v, r, s))
addr = Account.recover_transaction("0x" + tx_candidate.hex())
if is_valid_expected and EXPECTED_SENDER.lower() == addr.lower():
derived_sender = addr
final_v = try_v
encoded_tx = tx_candidate
break
if not is_valid_expected and derived_sender is None:
derived_sender = addr
final_v = try_v
encoded_tx = tx_candidate
print(f"Warning: EXPECTED_SENDER not provided or is placeholder. Assuming first recovered address: {addr}")
except Exception:
continue
if encoded_tx is None:
if is_valid_expected:
raise ValueError(f"Could not recover address matching EXPECTED_SENDER: {EXPECTED_SENDER}")
else:
raise ValueError("Could not derive broadcastable transaction. Ensure signature matches payload.")
if derived_sender:
print(f"Derived Sender Address: {derived_sender}")
else:
print("Derived Sender Address: Could not recover (ensure signature matches current transaction payload)")
valid_tx_hex = "0x" + encoded_tx.hex()
print(f"Broadcastable Transaction Hex:\n{valid_tx_hex}\n")
# ==========================================
# 6. BROADCAST TO NETWORK
# ==========================================
payload = {
"jsonrpc": "2.0",
"method": "eth_sendRawTransaction",
"params": [valid_tx_hex],
"id": 1,
}
print(f"Broadcasting to {RPC_URL}...")
response = requests.post(RPC_URL, json=payload)
resp_json = response.json()
print("RPC Response:", resp_json)
if "result" in resp_json:
tx_hash = resp_json["result"]
print(f"View transaction on Sepolia Etherscan: https://sepolia.etherscan.io/tx/{tx_hash}")
EOF
python3 broadcast_eth.py
일반적인 오류 문제 해결
invalid sender: recovery failed또는invalid transaction v, r, s values: 스크립트의 거래 매개변수가 서명된 페이로드와 정확히 일치하지 않는 경우에 발생합니다.value,nonce,to필드가 5단계에서 처리된 내용과 일치하는지 확인합니다.insufficient funds for gas * price + value: 트랜잭션이 암호화 방식으로 유효하며 노드에서 수락되었지만 MPC 키의 이더리움 주소에 작업을 처리하기에 충분한 테스트넷 ETH가 없습니다. 브로드캐스트하기 전에 주소에 자금을 입금해야 합니다.
비트코인
SegWit 송금 거래 조립 및 브로드캐스팅
비트코인 네이티브 SegWit 트랜잭션(BIP 141)의 경우 원시 64바이트 (r, s) MPC 서명을 표준 low-S ASN.1 DER로 형식을 지정하고 0x01(SIGHASH_ALL)를 추가한 후 33바이트 압축 공개 키와 함께 2요소 SegWit 증인 스택([DER signature || 0x01, compressed public key])으로 조합해야 합니다.
방송 스크립트:
다음 블록을 실행하여 broadcast_btc.py를 만들고 실행합니다.
cat <<'EOF' > broadcast_btc.py
"""Assembles a signed Bitcoin Native SegWit transaction from an MPC signature using embit and broadcasts it."""
import base64
import urllib.request
from embit import ec, script, transaction
# ==========================================
# 1. INPUTS
# ==========================================
FINAL_SIGNATURE_B64 = "your-final-signature"
COMPRESSED_PUBKEY_HEX = "03deb76948c14003446a8c1d79e88fb3607c9bbc1f5143b186aa7b61d6b1facc36"
PREV_TXID_HEX = "8f594f33156993f1c980c2273dcae4a92cf2517fbc29264421395e63a63916d8"
PREV_VOUT = int("1")
RECIPIENT_SATS = int("50000")
RECIPIENT_SCRIPTPUBKEY_HEX = "0014751e76e8199196d454941c45d1b3a323f1433bd6"
CHANGE_SATS = int("149750")
CHANGE_SCRIPTPUBKEY_HEX = "0014eced05cd51d964102a20c856b09080ad620e9da2"
BROADCAST_API_URL = "https://mempool.space/testnet4/api/tx"
# ==========================================
# 2. CONVERT RAW (r, s) SIGNATURE TO CANONICAL LOW-S ASN.1 DER + SIGHASH_ALL
# ==========================================
sig_bytes = base64.b64decode(FINAL_SIGNATURE_B64)
internal_sig = ec.secp256k1.ecdsa_signature_parse_compact(sig_bytes)
ec.secp256k1.ecdsa_signature_normalize(internal_sig, internal_sig)
der_sig = ec.secp256k1.ecdsa_signature_serialize_der(internal_sig) + b"\x01" # 0x01 = SIGHASH_ALL
# ==========================================
# 3. BUILD TRANSACTION & ATTACH SEGWIT WITNESS STACK
# ==========================================
tx = transaction.Transaction(
version=2,
vin=[transaction.TransactionInput(
txid=bytes.fromhex(PREV_TXID_HEX),
vout=PREV_VOUT,
sequence=0xFFFFFFFF,
)],
vout=[
transaction.TransactionOutput(
value=RECIPIENT_SATS,
script_pubkey=script.Script(bytes.fromhex(RECIPIENT_SCRIPTPUBKEY_HEX)),
),
transaction.TransactionOutput(
value=CHANGE_SATS,
script_pubkey=script.Script(bytes.fromhex(CHANGE_SCRIPTPUBKEY_HEX)),
),
],
locktime=0,
)
pubkey_bytes = bytes.fromhex(COMPRESSED_PUBKEY_HEX)
tx.vin[0].witness = transaction.Witness([der_sig, pubkey_bytes])
signed_tx_hex = tx.serialize().hex()
print("--- Signed Bitcoin SegWit Wire Transaction Hex ---")
print(signed_tx_hex)
# ==========================================
# 4. BROADCAST TO NETWORK
# ==========================================
if BROADCAST_API_URL:
print(f"\nBroadcasting to {BROADCAST_API_URL}...")
try:
req = urllib.request.Request(
BROADCAST_API_URL,
data=signed_tx_hex.encode("utf-8"),
headers={"Content-Type": "text/plain"},
method="POST",
)
with urllib.request.urlopen(req) as resp:
txid_result = resp.read().decode("utf-8").strip()
print(f"✅ Broadcast Successful! Transaction ID (txid): {txid_result}")
print(f"Track in Mempool Explorer: https://mempool.space/testnet4/tx/{txid_result}")
except Exception as e:
print(f"Broadcast response: {e}")
EOF
python3 broadcast_btc.py
일반적인 오류 문제 해결
mandatory-script-verify-flag-failed (Signature must be zero for failed CHECK(MULTI)SIG operation)/non-mandatory-script-verify-flag (Non-canonical DER signature): 서명의s구성요소가 표준 low-s 형식 (s <= N/2)이고 DER로 올바르게 인코딩되어 있는지 확인합니다.bad-txns-in-belowout/bad-txns-fee-negative: 총 출력이 입력 값을 초과합니다. 변경 계산 (InputSatoshis - PaymentSatoshis - MinerFee)이 양수인지 확인하세요.
📖 부록: 운영 가이드 (쿼리 및 검사)
이러한 읽기 전용 명령어를 사용하여 언제든지 리소스 상태를 검사할 수 있습니다.
👥 1. 참여자 및 서명자
인스턴스의 참여자와 서명자를 검사합니다.
A. 등록된 모든 참여자 나열
curl
curl -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/signers | jq
B. 특정 참여자의 세부정보 가져오기
curl
curl -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/signers/signer-alice | jq
📜 2. 정책 엔진
정책 및 트러스트 번들을 검사합니다.
A. 정책 작성자 트러스트 번들 가져오기
curl
curl -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policyAuthorTrustBundle | jq
B. 모든 정책 나열
curl
curl -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies | jq
C. 특정 정책의 세부정보 가져오기
curl
curl -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/ethereum-transfer-policy | jq
🔑 3. 키 및 키링
키링 및 키를 검사합니다.
A. 모든 KeyRing 나열
curl
curl -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings | jq
B. 키링의 모든 키 나열
curl
curl -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys | jq
C. 특정 분산 키의 상태 가져오기 (DKG 상태)
curl
curl -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key | jq
📝 4. 제안서 (서명 작업)
제안서 및 서명 상태를 검사합니다.
A. 키의 모든 제안 나열
curl
curl -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals | jq
B. 특정 제안서의 세부정보 또는 상태 가져오기
curl
curl -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals/my-proposal | jq