本指南說明如何使用 Multi-party Compute API 管理分散式金鑰、參與者和政策。適用於想瞭解主要使用者歷程的預先發布版客戶。
總覽
MPC API 包含兩個主要元件:
- Core MPC 會管理分散式金鑰、提案和簽署儀式的生命週期。
- 政策引擎會強制執行管理措施,在作業執行前進行授權。
如要詳細瞭解 API 存取權和加密簽署之間的互動方式,請參閱「安全性與授權」。
必要條件
開始前,請確認您具備以下項目:
- A Google Cloud 專案。如要建立專案,請參閱「建立專案」。
- 如果未使用 Cloud Shell,請安裝
gcloudCLI 並完成驗證。 啟用必要的 API (如尚未啟用)。您需要兩個 API,分別是本指南使用的全新 MPC API,以及用於產生/簽署同意聲明金鑰的 Cloud Key Management Service。
gcloud services enable mpc.googleapis.com cloudkms.googleapis.com如要查看已啟用的 API,請按照下列步驟操作:
gcloud services list --enabled已安裝 Python 3.9 以上版本和教學課程輔助程式庫:
pip install embit eth-account eth-utils requests(選用) 如果貴組織使用外部身分識別提供者 (例如 Okta 或 Microsoft Entra ID) 進行驗證,而非 Google 帳戶,請參閱「設定員工身分聯盟 (BYOID)」,瞭解如何使用聯合身分設定應用程式預設憑證 (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 Key Management Service KeyRing,用於儲存在 Cloud KMS 的外部同意聲明金鑰。 |
| 簽署人 1 ID | signer-alice |
MPC 簽署者 1。 |
| 簽署人 2 ID | signer-bob |
MPC 簽署者 2。 |
| 簽署者 3 ID | signer-charlie |
MPC 簽署者 3。 |
| Policy Author 1 ID | author-dave |
政策作者 1。 |
| Policy Author 2 ID | author-eve |
政策作者 2。 |
| 政策 ID | ethereum-transfer-policy |
政策 ID (例如 ethereum-transfer-policy 或 bitcoin-psbt-policy)。 |
| KeyRing ID | my-key-ring |
KeyRing ID。 |
| 金鑰 ID | my-key |
金鑰 ID。 |
| 提案 ID | my-proposal |
提案 ID。 |
步驟 1:產生 Assent 金鑰
如要使用 MPC API,您必須為下列不同角色產生金鑰組。請注意,這些是外部加密同意金鑰,與設定期間內部產生的分散式 MPC 門檻金鑰碎片不同。
- 政策作者:授權管理政策。
- 簽署者:對提案表示同意 (投票/核准)。
A. 參與者金鑰 (secp256k1)
為參與者產生必要的加密編譯金鑰。這些指令具有冪等性。
Cloud KMS
建議您在 Cloud Key Management Service 中管理同意聲明金鑰。確認您的身分具備建立金鑰環、建立金鑰、簽署及查看公開金鑰驗證器的權限 (例如 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 中管理這些同意金鑰。確認您的身分具備建立金鑰環、建立金鑰、簽署及查看公開金鑰驗證器的權限 (例如 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),並建立規則,說明允許執行的動作。您可以設定多項政策來管理不同情境,但本指南僅示範如何設定以太幣或比特幣交易政策。根據預設,政策引擎會快速失敗 (預設封鎖);如果沒有政策明確允許交易,交易就會遭到拒絕。
建立或核准政策前,作者必須簽署代表意圖的酬載。您可以使用下列輔助函式,處理二進位序列化和簽署作業。
Ethereum
這項規則允許將較小額的以太幣 (<= 2 ETH,以 2,000,000,000 Gwei 表示) 轉移至 Alice 的特定地址。
1. 產生 Quorum 簽名
使用 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,或 0.0025 BTC,正規化為 10-9 固定小數位數單位),且礦工手續費不超過 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. 產生 Quorum 簽名
使用 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. 核准政策 (轉換為「有效」)
如果政策設定需要 M-of-N 簽章,後續作者會呼叫 AddQuorumSignature。
如要產生簽章,您必須先擷取政策目前的序列。
Ethereum
# 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 位元組未壓縮公開金鑰,衍生出區塊鏈專屬位址,並找出交易所需的未花費資金。
Ethereum
以太坊位址衍生自 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 廣播前,請確認衍生出的 Ethereum 位址有足夠的 Sepolia 測試網 ETH,可支付交易金額和燃料費。只要在 Faucet for Ethereum Sepolia 輸入新產生的位址,即可索取免費的測試網 ETH。
比特幣
比特幣位址衍生自未壓縮的 65 位元組 secp256k1 EC 公開金鑰 (0x04 || X || Y)。同一個公開金鑰支援多種位址格式:
- 原生隔離見證 (P2WPKH / BIP 173):Bech32 位址 (主網為
bc1q...,Testnet4 為tb1q...),具有 22 位元組見證scriptPubKey(0014<hash160>)。這是建議使用的現代格式,交易手續費最低。 - 巢狀 SegWit (P2SH-P2WPKH / BIP 49):包裝 SegWit 位址 (主網為
3...,Testnet4 為2...),具有 23 位元組scriptPubKey(a914<hash160>87) 和 22 位元組 redeemScript (0014<hash160>),與舊版系統相容。 - 舊版 (P2PKH/BIP 44):原始 Pay-to-PubKey-Hash Base58 位址 (主網為
1...,Testnet4 為m/n...),具有 25 位元組scriptPubKey(76a914<hash160>88ac)。
位址衍生指令碼 (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 建構交易前:
- 為衍生出的原生隔離見證地址提供資金 (例如使用測試網水龍頭)。
- 使用比特幣節點 (
bitcoin-cli listunspent 1 9999999 "[\"tb1q...\"]") 或區塊瀏覽器 API,查詢地址未花費的 UTXO。 - 請記下以下 4 個參數,以便在步驟 5 使用:
txid:未花費輸出內容的 32 位元組交易雜湊。vout:以零為索引的輸出索引 (例如0或1)。value:未花費的聰數 (例如200000聰 = 0.002 BTC)。scriptPubKey:鎖定指令碼十六進位值 (例如0014...)。
步驟 5:執行 MPC 作業
評估交易並建立提案,執行 MPC 作業。
A. 評估交易 (取得授權權杖)
簽署 MPC 作業前,請先根據政策授權酬載,取得 JWS 權杖和 32 位元組簽署摘要。
Ethereum
🛑 情況 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:核准 (允許值)
嘗試授權 1 Gwei 的交易 (在 2 ETH 限制內)。
# 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) 中,計算簽章雜湊時,必須知道所花費輸入內容的聰數量和鎖定指令碼。
政策引擎需要 BIP 174 PSBT v0 格式 (BITCOIN_TX_V2),且每個輸入內容都必須填入 PSBT_IN_WITNESS_UTXO。
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'))")
預期回應:decision: PERMIT,以及 JWS token和 32 位元組 BIP 143 sighash digest。
這會傳回:
decision:PERMIT或DENYtoken:JWS 政策權杖 (法規遵循證明)。digest:十六進位編碼的 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 後,最後一個步驟是建構已簽署的電匯交易,並廣播至網路。
Ethereum
復原 ID 衍生和廣播
MPC API 會傳回原始 64 位元組 ECDSA 簽章 (包含 r 和 s 值)。伺服器會確保簽章採用標準 low-s 格式 (EIP-2)。不過,Ethereum 需要復原 ID (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 金鑰的 Ethereum 位址沒有足夠的測試網 ETH 來支付作業費用。你必須先為地址提供資金,才能廣播。
比特幣
SegWit 電匯交易組合與廣播
如果是比特幣原生隔離見證交易 (BIP 141),原始 64 位元組 (r, s) MPC 簽章必須採用標準低 S ASN.1 DER 格式,並附加 0x01 (SIGHASH_ALL),然後與 33 位元組壓縮公開金鑰組合成 2 元素隔離見證堆疊:[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