本指南將逐步說明多方運算 API 的使用者歷程,協助您管理分散式金鑰、使用者、團隊和政策。適用於想瞭解主要使用者歷程的預先發布版客戶。
總覽
MPC API 可管理執行個體、團隊、使用者、金鑰環、分散式金鑰和門檻簽署儀式的生命週期,並透過三個政策層強制執行內建的加密編譯管理機制:
- 管理政策 (角色法定人數):管理員會將使用者和團隊加入系統,且必須有法定人數的管理員 (
EnableUserRole和ApproveRoleChange) 才能授予或撤銷管理員和政策作者角色。授予簽署者角色時,單一管理員即可立即生效,因為簽署者本身無法存取現有金鑰;只有在產生金鑰時,簽署者包含在金鑰環的金鑰政策中,才會收到金鑰共用。 - 金鑰政策 (
KeyRing.keyPolicy):管理員在建立 KeyRing 時,會定義 KeyRing 的 KeyPolicy,指定哪些簽署者 (簽署者,分組為團隊) 持有金鑰共用,以及授權簽章所需的 K-of-N 簽署者核准 (CreateTransaction和RecordSignerDecision)。 - 交易政策 (
TransactionPolicy和KeyList):政策作者 (POLICY_AUTHOR) 的法定人數提議、核准或停用 Cedarpermit和forbid規則 (TransactionPolicy) 和KeyList位址許可清單。系統會先根據有效的交易政策 (PERMIT或DENY) 評估每個CreateTransaction要求,再進行簽署。
必要條件
開始前,請確認您具備以下項目:
- 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 |
MPC 執行個體的消費者專案 ID。 |
| 執行個體 ID | my-mpc-instance |
MPC 執行個體的 ID ([a-z]([a-z0-9-]{0,61}[a-z0-9])?)。 |
| 位置 | us-central1 |
部署位置 (例如 us-central1)。 |
| 管理員 1 主體 | admin-1@example.com |
執行個體中第一個 ADMIN 使用者的Google Cloud 電子郵件或 BYOID 主體。 |
| 管理員 2 主體 | admin-2@example.com |
執行個體中第二位 ADMIN 使用者的Google Cloud 電子郵件或 BYOID 主體。 |
| 簽署者 1 主體 | alice@example.com |
簽署者 1 (Alice) 的Google Cloud 電子郵件或 BYOID 主體。 |
| 簽署人 2 主體 | bob@example.com |
Google Cloud 電子郵件地址或BYOID 主體。 |
| 簽署者 3 主體 | charlie@example.com |
簽署者 3 (Charlie) 的Google Cloud 電子郵件或 BYOID 主體。 |
| 政策作者 1 主體 | dave@example.com |
Google Cloud 政策作者 1 (Dave) 的電子郵件地址或 BYOID 主體。 |
| 政策作者 2 主體 | eve@example.com |
Google Cloud 政策作者 2 (Eve) 的電子郵件地址或 BYOID 主體。 |
在終端機工作階段中驗證每個帳戶一次,這樣 gcloud auth print-access-token "<principal>" 就能擷取任何角色的權杖,不必在教學課程中切換帳戶 (ADMIN_1_PRINCIPAL 是最後驗證的帳戶,因此仍是 IAM 和 Cloud KMS 指令的作用中 gcloud CLI 帳戶):
for PRINCIPAL in \
"admin-2@example.com" \
"alice@example.com" \
"bob@example.com" \
"charlie@example.com" \
"dave@example.com" \
"eve@example.com" \
"admin-1@example.com"; do
gcloud auth login "$PRINCIPAL"
done
教學課程資源標籤
這些是本教學課程中使用 Cloud KMS 建立的授權金鑰本機 ID。預設值有效,可保留以盡量減少設定阻礙。請注意,Team、User、TransactionPolicy、KeyRing、Key、KeyList 和 Transaction 資源的資源 ID 是由伺服器產生,並在建立各項資源時擷取至殼層變數 (TEAM_NAME、SIGNER_1_NAME、POLICY_NAME、KEY_RING_NAME、KEY_NAME、KEY_LIST_NAME、TRANSACTION_NAME)。
| 參數 | 值 | 說明 |
|---|---|---|
| Cloud KMS 金鑰環 ID | mpc-assent-keys |
Cloud Key Management Service KeyRing,用於儲存在 Cloud KMS 的外部授權金鑰。 |
| Cloud KMS 管理員 1 ID | admin-1 |
MPC 管理員 1 授權金鑰標籤。 |
| Cloud KMS 管理員 2 ID | admin-2 |
MPC 管理員 2 授權金鑰標籤。 |
| Cloud KMS 簽署者 1 ID | signer-alice |
MPC 簽署者 1 授權金鑰標籤。 |
| Cloud KMS 簽署者 2 ID | signer-bob |
MPC 簽署者 2 授權金鑰標籤。 |
| Cloud KMS 簽署者 3 ID | signer-charlie |
MPC 簽署者 3 授權金鑰標籤。 |
| Cloud KMS 政策作者 1 ID | author-dave |
政策作者 1 授權金鑰標籤。 |
| Cloud KMS 政策作者 2 ID | author-eve |
政策作者 2 授權金鑰標籤。 |
步驟 1:建立 MPC 執行個體
MPC Instance 代表專屬的隔離 MPC 環境。您必須具備專案的多方運算管理員 (roles/mpc.admin) IAM 角色,才能建立及管理執行個體。您可以使用控制層端點 (https://mpc.googleapis.com) 上的 MPCInstanceService,以管理員身分建立及檢查 Instance,並將所有後續資源要求傳送至執行個體的專屬資料層網址 (INSTANCE_URL)。
A. 建立執行個體
首先,請確認管理員主體 (admin-1@example.com 和 admin-2@example.com) 具備專案的 roles/mpc.admin 權限,並以管理員 1 的身分呼叫 instances.create (CreateInstance),佈建 MPC 執行個體:
curl
grant_iam_role() {
local principal=$1
local iam_role=$2
local member="user:${principal}"
# BYOID principals already start with "principal://"
if [[ "$principal" == principal://* ]]; then
member="${principal}"
fi
echo "Granting ${iam_role} to ${member}..."
gcloud projects add-iam-policy-binding "my-project" \
--member="${member}" \
--role="${iam_role}" \
--condition=None > /dev/null
}
for ADMIN_PRINCIPAL in "admin-1@example.com" "admin-2@example.com"; do
grant_iam_role "$ADMIN_PRINCIPAL" "roles/mpc.admin"
done
export INSTANCE_NAME="projects/my-project/locations/us-central1/instances/my-mpc-instance"
# Check existence
if curl -s -f -o /dev/null -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
"https://mpc.googleapis.com/v1beta/${INSTANCE_NAME}"; then
echo "Instance 'my-mpc-instance' already exists. Skipping creation."
else
echo "Instance 'my-mpc-instance' not found. Creating..."
curl -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"displayName": "Tutorial MPC Instance"
}' \
"https://mpc.googleapis.com/v1beta/projects/my-project/locations/us-central1/instances?instanceId=my-mpc-instance" | jq
fi
這會傳回長時間執行的作業 (LRO)。您必須等待執行個體 state 變成 ACTIVE,才能呼叫其資料層網址。
🕵️♂️ 檢查執行個體狀態 (輪詢)
輪詢執行個體狀態,直到 state 變成 ACTIVE,然後將 url 欄位儲存至 INSTANCE_URL:
curl
INSTANCE_INFO=$(curl -s -X GET \
-H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
"https://mpc.googleapis.com/v1beta/${INSTANCE_NAME}")
echo "$INSTANCE_INFO" | jq
export INSTANCE_URL=$(echo "$INSTANCE_INFO" | jq -r '.url')
echo "Instance Data Plane URL: $INSTANCE_URL"
步驟 2:產生授權金鑰
如要使用 MPC API,您必須為下列不同角色產生金鑰組。請注意,這些是外部加密授權金鑰 (User.authorizationKey),也就是說,您是在 MPC 執行個體外部產生及保管這些金鑰,因此與建立金鑰時產生的 MPC 金鑰共用不同:
- 管理員:授權變更
ADMIN和POLICY_AUTHOR使用者角色,以及建立Key。 - 政策作者:授權管理政策 (
TransactionPolicy)。 - 簽署者:提供交易授權 (
AUTHORIZE或DENY)。
為簽署者、政策作者和管理員產生 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 Signers, Policy Authors, and Admins
for ID in \
signer-alice \
signer-bob \
signer-charlie \
author-dave \
author-eve \
admin-1 \
admin-2; 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 \
author-dave \
author-eve \
admin-1 \
admin-2; do
echo "Generating OpenSSL key for $ID..."
provision_openssl_key "$ID"
done
步驟 3:註冊團隊和使用者
任何實體都必須具備專案的 IAM 權限,並在執行個體中註冊為應用程式層級的 User,才能參與交易簽署 (做為 SIGNER) 或政策編寫 (做為 POLICY_AUTHOR)。如要瞭解 IAM 角色和 User 角色如何搭配運作,請參閱「安全性與授權」。
註冊時,管理員和使用者必須透過頻外方式協調:
- 實體 (簽署者或政策作者) 在本機產生金鑰組,並將公開金鑰安全地轉移給管理員。
- 管理員在專案中授予使用者主體適當的 IAM 角色 (簽署者和政策作者為
roles/mpc.editor;管理員為roles/mpc.admin),並呼叫 MPC API 來註冊使用者的principal和authorizationKey。
A. 建立簽署者團隊
每個具有 SIGNER 角色的 User 都必須屬於 Team,而所有其他具有其他角色的 User 則不得屬於 Team。以管理員 1 (admin-1@example.com) 的身分,建立「Treasury Signers」Team (CreateTeam) 來存放三位簽署者,並擷取伺服器產生的資源 name:
curl
TEAM_RESPONSE=$(curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"displayName": "Treasury Signers",
}' \
"${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/teams?requestId=11111111-1111-4111-8111-111111111101")
echo "$TEAM_RESPONSE" | jq
export TEAM_NAME=$(echo "$TEAM_RESPONSE" | jq -r '.name')
echo "Created Team: $TEAM_NAME"
B. 授予 IAM 角色並註冊簽署者和政策作者
每位使用者都需要專案層級 IAM 角色 (簽署者和政策作者為 roles/mpc.editor,管理員為 roles/mpc.admin),才能呼叫 MPC API 和執行個體中的應用程式層級 User 資源。呼叫 users.create (CreateUser) 時,roles 欄位只能留空或設為 ["SIGNER"]:
- 簽署者:在 IAM 中授予
roles/mpc.editor,並使用roles: ["SIGNER"]和team: "$TEAM_NAME"建立。SIGNER角色會立即變成ACTIVE,不需經過管理員仲裁核准,因為單獨擁有SIGNER角色並不會授予任何現有金鑰的存取權;簽署者只有在金鑰產生期間,被納入KeyRing的KeyPolicy時,才會收到金鑰共用。 政策作者:在 IAM 中授予
roles/mpc.editor,建立時沒有初始角色和team,然後在步驟 3.C 中使用管理員法定人數核准授予POLICY_AUTHOR角色 (因為POLICY_AUTHOR和ADMIN具有執行個體層級的管理權限)。
curl
provision_user() {
local id=$1
local display_name=$2
local principal=$3
local request_id=$4
local team=$5
local pubkey_b64
local signer_fields=""
# 1. Grant project-level IAM permission (roles/mpc.editor) to the User's principal
grant_iam_role "$principal" "roles/mpc.editor" >&2
# 2. Read the public key from the file generated in Step 2 (Base64-encoded PEM/DER bytes)
pubkey_b64=$(openssl base64 -A -in "${id}_pub.pem")
if [[ -n "$team" ]]; then
signer_fields=", \"roles\": [\"SIGNER\"], \"team\": \"${team}\""
fi
# 3. Register the application-level User in the MPC instance as Admin 1
curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d "{
\"displayName\": \"${display_name}\",
\"principal\": \"${principal}\",
\"authorizationKey\": {
\"publicKey\": \"${pubkey_b64}\",
\"algorithm\": \"PUBLIC_KEY_SIGNATURE_ALGORITHM_ECDSA_SECP256K1\"
}${signer_fields}
}" \
"${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/users?requestId=${request_id}"
}
echo "Registering Signers..."
export SIGNER_1_NAME=$(provision_user "signer-alice" "Signer Alice" "alice@example.com" "11111111-1111-4111-8111-111111111001" "${TEAM_NAME}" | tee /dev/stderr | jq -r '.name')
export SIGNER_2_NAME=$(provision_user "signer-bob" "Signer Bob" "bob@example.com" "11111111-1111-4111-8111-111111111002" "${TEAM_NAME}" | tee /dev/stderr | jq -r '.name')
export SIGNER_3_NAME=$(provision_user "signer-charlie" "Signer Charlie" "charlie@example.com" "11111111-1111-4111-8111-111111111003" "${TEAM_NAME}" | tee /dev/stderr | jq -r '.name')
echo "Registering Policy Authors..."
export POLICY_AUTHOR_1_NAME=$(provision_user "author-dave" "Author Dave" "dave@example.com" "11111111-1111-4111-8111-111111111004" | tee /dev/stderr | jq -r '.name')
export POLICY_AUTHOR_2_NAME=$(provision_user "author-eve" "Author Eve" "eve@example.com" "11111111-1111-4111-8111-111111111005" | tee /dev/stderr | jq -r '.name')
C. 授予政策作者角色 (管理員法定人數核准)
如要啟用或停用User的POLICY_AUTHOR或ADMIN角色,必須取得管理員法定人數同意:
- 管理員 1 呼叫
users.enableRole(EnableUserRole),將角色移至PENDING_ACTIVE,並將管理員 1 的簽章記錄為第一項核准。 - 管理員 2 使用簽名呼叫
users.approveRoleChange(ApproveRoleChange),將角色轉換為ACTIVE。
Cloud KMS
1. 以管理員 1 (EnableUserRole) 的身分要求 POLICY_AUTHOR
sign_role_change() {
local admin_id=$1
local target_id=$2
local principal=$3
python3 -c 'import sys; inst, prin, pub = sys.argv[1], sys.argv[2], open(sys.argv[3], "rb").read(); v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("role_payload.bin", "wb").write(f(1, inst.encode()) + f(2, prin.encode()) + f(3, f(1, pub) + b"\x10\x01") + f(4, b"\x08\x02\x10\x01\x18\x01"))' "${INSTANCE_NAME}" "${principal}" "${target_id}_pub.pem"
gcloud kms asymmetric-sign \
--version=1 \
--key="${admin_id}-key" \
--keyring="mpc-assent-keys" \
--location="us-central1" \
--digest-algorithm=sha256 \
--input-file=role_payload.bin \
--signature-file=role_sig.bin > /dev/null
openssl base64 -A -in role_sig.bin
}
enable_policy_author() {
local target_user=$1
local target_id=$2
local target_principal=$3
echo "Admin 1 requesting POLICY_AUTHOR for ${target_user}..."
local admin_1_sig=$(sign_role_change "admin-1" "${target_id}" "${target_principal}")
local signed_payload_b64=$(openssl base64 -A -in role_payload.bin)
curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d "{
\"role\": \"POLICY_AUTHOR\",
\"authorizationInfo\": {
\"signature\": \"${admin_1_sig}\",
\"signedPayload\": \"${signed_payload_b64}\"
}
}" \
"${INSTANCE_URL}/v1beta/${target_user}:enableRole" | jq
}
enable_policy_author "${POLICY_AUTHOR_1_NAME}" "author-dave" "dave@example.com"
enable_policy_author "${POLICY_AUTHOR_2_NAME}" "author-eve" "eve@example.com"
2. POLICY_AUTHOR以管理員 2 (ApproveRoleChange) 的身分核准
approve_policy_author() {
local target_user=$1
local target_id=$2
local target_principal=$3
echo "Admin 2 approving POLICY_AUTHOR for ${target_user}..."
local admin_2_sig=$(sign_role_change "admin-2" "${target_id}" "${target_principal}")
local signed_payload_b64=$(openssl base64 -A -in role_payload.bin)
curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "admin-2@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d "{
\"authorizationInfo\": {
\"signature\": \"${admin_2_sig}\",
\"signedPayload\": \"${signed_payload_b64}\"
}
}" \
"${INSTANCE_URL}/v1beta/${target_user}:approveRoleChange" | jq
}
approve_policy_author "${POLICY_AUTHOR_1_NAME}" "author-dave" "dave@example.com"
approve_policy_author "${POLICY_AUTHOR_2_NAME}" "author-eve" "eve@example.com"
OpenSSL
1. 以管理員 1 (EnableUserRole) 的身分要求 POLICY_AUTHOR
sign_role_change() {
local admin_id=$1
local target_id=$2
local principal=$3
python3 -c 'import sys; inst, prin, pub = sys.argv[1], sys.argv[2], open(sys.argv[3], "rb").read(); v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("role_payload.bin", "wb").write(f(1, inst.encode()) + f(2, prin.encode()) + f(3, f(1, pub) + b"\x10\x01") + f(4, b"\x08\x02\x10\x01\x18\x01"))' "${INSTANCE_NAME}" "${principal}" "${target_id}_pub.pem"
openssl dgst -sha256 -sign "${admin_id}_priv.pem" -out role_sig.bin role_payload.bin
openssl base64 -A -in role_sig.bin
}
enable_policy_author() {
local target_user=$1
local target_id=$2
local target_principal=$3
echo "Admin 1 requesting POLICY_AUTHOR for ${target_user}..."
local admin_1_sig=$(sign_role_change "admin-1" "${target_id}" "${target_principal}")
local signed_payload_b64=$(openssl base64 -A -in role_payload.bin)
curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d "{
\"role\": \"POLICY_AUTHOR\",
\"authorizationInfo\": {
\"signature\": \"${admin_1_sig}\",
\"signedPayload\": \"${signed_payload_b64}\"
}
}" \
"${INSTANCE_URL}/v1beta/${target_user}:enableRole" | jq
}
enable_policy_author "${POLICY_AUTHOR_1_NAME}" "author-dave" "dave@example.com"
enable_policy_author "${POLICY_AUTHOR_2_NAME}" "author-eve" "eve@example.com"
2. POLICY_AUTHOR以管理員 2 (ApproveRoleChange) 的身分核准
approve_policy_author() {
local target_user=$1
local target_id=$2
local target_principal=$3
echo "Admin 2 approving POLICY_AUTHOR for ${target_user}..."
local admin_2_sig=$(sign_role_change "admin-2" "${target_id}" "${target_principal}")
local signed_payload_b64=$(openssl base64 -A -in role_payload.bin)
curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "admin-2@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d "{
\"authorizationInfo\": {
\"signature\": \"${admin_2_sig}\",
\"signedPayload\": \"${signed_payload_b64}\"
}
}" \
"${INSTANCE_URL}/v1beta/${target_user}:approveRoleChange" | jq
}
approve_policy_author "${POLICY_AUTHOR_1_NAME}" "author-dave" "dave@example.com"
approve_policy_author "${POLICY_AUTHOR_2_NAME}" "author-eve" "eve@example.com"
步驟 4:設定交易政策
建立並核准 TransactionPolicy (以及選用的 KeyList),設定交易政策。
A. 建立政策 (PENDING_ACTIVE)
建立 Cedar 政策規則,說明允許哪些交易。您可以設定多項政策來管理不同情境,但本指南僅示範如何設定以太幣或比特幣交易政策。根據預設,交易政策評估為「快速失敗 (預設封鎖)」;如果沒有任何有效的 permit 政策與交易相符,政策評估決策為 DENY。如要進一步瞭解 Cedar 屬性,請參閱「編寫交易政策規則」。
Ethereum、KMS
這項規則允許將較小金額的以太幣 (<= 2 ETH,以 10-9 ETH / Gwei 為單位,表示為 2,000,000,000) 轉移至特定收款人地址 (0x1111111111111111111111111111111111111111)。
permit (principal, action, resource)
when {
context.schema == "ETHEREUM_TX_V2" &&
context.secondary_key == "0x1111111111111111111111111111111111111111" &&
context has amount_token_scaled &&
context.amount_token_scaled <= 2000000000
};
sign_policy() {
local author_id=$1
local resource_type=$2
local resource_name=$3
local action=$4
local cedar_doc=$5
python3 -c 'import sys; rt, rn, act, doc = sys.argv[1:]; v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("policy_payload.bin", "wb").write(f(1 if rt == "instance" else 2, rn.encode()) + bytes([24, 1 if act == "ACTIVATE" else 2]) + f(4, f(1, doc.encode())))' "${resource_type}" "${resource_name}" "${action}" "${cedar_doc}"
gcloud kms asymmetric-sign \
--version=1 \
--key="${author_id}-key" \
--keyring="mpc-assent-keys" \
--location="us-central1" \
--digest-algorithm=sha256 \
--input-file=policy_payload.bin \
--signature-file=policy_sig.bin > /dev/null
openssl base64 -A -in policy_sig.bin
}
export CEDAR_RULE='permit (principal, action, resource) when { context.schema == "ETHEREUM_TX_V2" && context.secondary_key == "0x1111111111111111111111111111111111111111" && context has amount_token_scaled && context.amount_token_scaled <= 2000000000 };'
# Sign as Policy Author 1
QUORUM_SIGNATURE=$(sign_policy "author-dave" "instance" "${INSTANCE_NAME}" "ACTIVATE" "${CEDAR_RULE}")
POLICY_PAYLOAD_B64=$(openssl base64 -A -in policy_payload.bin)
POLICY_RESPONSE=$(jq -n \
--arg rule "$CEDAR_RULE" \
--arg sig "$QUORUM_SIGNATURE" \
--arg payload "$POLICY_PAYLOAD_B64" \
'{
transactionPolicy: {
displayName: "Ethereum Transfer Permit Policy",
rule: {cedarDocument: $rule}
},
authorizationInfo: {
signature: $sig,
signedPayload: $payload
},
requestId: "11111111-1111-4111-8111-111111111201"
}' \
| curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "dave@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d @- \
"${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/transactionPolicies")
echo "$POLICY_RESPONSE" | jq
export POLICY_NAME=$(echo "$POLICY_RESPONSE" | jq -r '.name')
Ethereum、OpenSSL
這項規則允許將較小金額的以太幣 (<= 2 ETH,以 10-9 ETH / Gwei 為單位,表示為 2,000,000,000) 轉移至特定收款人地址 (0x1111111111111111111111111111111111111111)。
permit (principal, action, resource)
when {
context.schema == "ETHEREUM_TX_V2" &&
context.secondary_key == "0x1111111111111111111111111111111111111111" &&
context has amount_token_scaled &&
context.amount_token_scaled <= 2000000000
};
sign_policy() {
local author_id=$1
local resource_type=$2
local resource_name=$3
local action=$4
local cedar_doc=$5
python3 -c 'import sys; rt, rn, act, doc = sys.argv[1:]; v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("policy_payload.bin", "wb").write(f(1 if rt == "instance" else 2, rn.encode()) + bytes([24, 1 if act == "ACTIVATE" else 2]) + f(4, f(1, doc.encode())))' "${resource_type}" "${resource_name}" "${action}" "${cedar_doc}"
openssl dgst -sha256 -sign "${author_id}_priv.pem" -out policy_sig.bin policy_payload.bin
openssl base64 -A -in policy_sig.bin
}
export CEDAR_RULE='permit (principal, action, resource) when { context.schema == "ETHEREUM_TX_V2" && context.secondary_key == "0x1111111111111111111111111111111111111111" && context has amount_token_scaled && context.amount_token_scaled <= 2000000000 };'
# Sign as Policy Author 1
QUORUM_SIGNATURE=$(sign_policy "author-dave" "instance" "${INSTANCE_NAME}" "ACTIVATE" "${CEDAR_RULE}")
POLICY_PAYLOAD_B64=$(openssl base64 -A -in policy_payload.bin)
POLICY_RESPONSE=$(jq -n \
--arg rule "$CEDAR_RULE" \
--arg sig "$QUORUM_SIGNATURE" \
--arg payload "$POLICY_PAYLOAD_B64" \
'{
transactionPolicy: {
displayName: "Ethereum Transfer Permit Policy",
rule: {cedarDocument: $rule}
},
authorizationInfo: {
signature: $sig,
signedPayload: $payload
},
requestId: "11111111-1111-4111-8111-111111111201"
}' \
| curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "dave@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d @- \
"${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/transactionPolicies")
echo "$POLICY_RESPONSE" | jq
export POLICY_NAME=$(echo "$POLICY_RESPONSE" | jq -r '.name')
Bitcoin、KMS
這項規則允許使用 BITCOIN_TX_V2 結構定義 (BIP 174 PSBT v0) 轉移比特幣,前提是淨轉出金額不超過 250,000 聰 (context.amount_token_scaled <= 2500000,或 0.0025 BTC 標準化為 10-9 固定小數位數單位),且礦工手續費不超過 10,000 聰 (context.max_fee_atomic <= 100000)。
permit (principal, action, resource)
when {
context.schema == "BITCOIN_TX_V2" &&
context has amount_token_scaled &&
context.amount_token_scaled <= 2500000 &&
context.max_fee_atomic <= 100000
};
sign_policy() {
local author_id=$1
local resource_type=$2
local resource_name=$3
local action=$4
local cedar_doc=$5
python3 -c 'import sys; rt, rn, act, doc = sys.argv[1:]; v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("policy_payload.bin", "wb").write(f(1 if rt == "instance" else 2, rn.encode()) + bytes([24, 1 if act == "ACTIVATE" else 2]) + f(4, f(1, doc.encode())))' "${resource_type}" "${resource_name}" "${action}" "${cedar_doc}"
gcloud kms asymmetric-sign \
--version=1 \
--key="${author_id}-key" \
--keyring="mpc-assent-keys" \
--location="us-central1" \
--digest-algorithm=sha256 \
--input-file=policy_payload.bin \
--signature-file=policy_sig.bin > /dev/null
openssl base64 -A -in policy_sig.bin
}
export CEDAR_RULE='permit (principal, action, resource) when { context.schema == "BITCOIN_TX_V2" && context has amount_token_scaled && context.amount_token_scaled <= 2500000 && context.max_fee_atomic <= 100000 };'
# Sign as Policy Author 1
QUORUM_SIGNATURE=$(sign_policy "author-dave" "instance" "${INSTANCE_NAME}" "ACTIVATE" "${CEDAR_RULE}")
POLICY_PAYLOAD_B64=$(openssl base64 -A -in policy_payload.bin)
POLICY_RESPONSE=$(jq -n \
--arg rule "$CEDAR_RULE" \
--arg sig "$QUORUM_SIGNATURE" \
--arg payload "$POLICY_PAYLOAD_B64" \
'{
transactionPolicy: {
displayName: "Bitcoin PSBT Permit Policy",
rule: {cedarDocument: $rule}
},
authorizationInfo: {
signature: $sig,
signedPayload: $payload
},
requestId: "11111111-1111-4111-8111-111111111202"
}' \
| curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "dave@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d @- \
"${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/transactionPolicies")
echo "$POLICY_RESPONSE" | jq
export POLICY_NAME=$(echo "$POLICY_RESPONSE" | jq -r '.name')
Bitcoin、OpenSSL
這項規則允許使用 BITCOIN_TX_V2 結構定義 (BIP 174 PSBT v0) 轉移比特幣,前提是淨轉出金額不超過 250,000 聰 (context.amount_token_scaled <= 2500000,或 0.0025 BTC 標準化為 10-9 固定小數位數單位),且礦工手續費不超過 10,000 聰 (context.max_fee_atomic <= 100000)。
permit (principal, action, resource)
when {
context.schema == "BITCOIN_TX_V2" &&
context has amount_token_scaled &&
context.amount_token_scaled <= 2500000 &&
context.max_fee_atomic <= 100000
};
sign_policy() {
local author_id=$1
local resource_type=$2
local resource_name=$3
local action=$4
local cedar_doc=$5
python3 -c 'import sys; rt, rn, act, doc = sys.argv[1:]; v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("policy_payload.bin", "wb").write(f(1 if rt == "instance" else 2, rn.encode()) + bytes([24, 1 if act == "ACTIVATE" else 2]) + f(4, f(1, doc.encode())))' "${resource_type}" "${resource_name}" "${action}" "${cedar_doc}"
openssl dgst -sha256 -sign "${author_id}_priv.pem" -out policy_sig.bin policy_payload.bin
openssl base64 -A -in policy_sig.bin
}
export CEDAR_RULE='permit (principal, action, resource) when { context.schema == "BITCOIN_TX_V2" && context has amount_token_scaled && context.amount_token_scaled <= 2500000 && context.max_fee_atomic <= 100000 };'
# Sign as Policy Author 1
QUORUM_SIGNATURE=$(sign_policy "author-dave" "instance" "${INSTANCE_NAME}" "ACTIVATE" "${CEDAR_RULE}")
POLICY_PAYLOAD_B64=$(openssl base64 -A -in policy_payload.bin)
POLICY_RESPONSE=$(jq -n \
--arg rule "$CEDAR_RULE" \
--arg sig "$QUORUM_SIGNATURE" \
--arg payload "$POLICY_PAYLOAD_B64" \
'{
transactionPolicy: {
displayName: "Bitcoin PSBT Permit Policy",
rule: {cedarDocument: $rule}
},
authorizationInfo: {
signature: $sig,
signedPayload: $payload
},
requestId: "11111111-1111-4111-8111-111111111202"
}' \
| curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "dave@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d @- \
"${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/transactionPolicies")
echo "$POLICY_RESPONSE" | jq
export POLICY_NAME=$(echo "$POLICY_RESPONSE" | jq -r '.name')
B. 核准政策 (轉換為「有效」)
政策變更需要 2 個簽名,因此第二位政策作者必須使用已簽署的 TransactionPolicySigningPayload 呼叫 transactionPolicies.approve (ApproveTransactionPolicy)。
Ethereum
# 1. Check the policy state
POLICY_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth print-access-token "eve@example.com")" \
-H "X-Goog-User-Project: my-project" \
"${INSTANCE_URL}/v1beta/${POLICY_NAME}")
POLICY_STATE=$(echo "$POLICY_INFO" | jq -r '.state // "UNKNOWN"')
if [ "$POLICY_STATE" == "PENDING_ACTIVE" ]; then
# 2. Sign as Policy Author 2 naming transaction_policy ($POLICY_NAME)
QUORUM_SIGNATURE_2=$(sign_policy "author-eve" "transaction_policy" "${POLICY_NAME}" "ACTIVATE" "${CEDAR_RULE}")
POLICY_PAYLOAD_2_B64=$(openssl base64 -A -in policy_payload.bin)
# 3. Call ApproveTransactionPolicy as Policy Author 2
curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "eve@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d "{
\"authorizationInfo\": {
\"signature\": \"${QUORUM_SIGNATURE_2}\",
\"signedPayload\": \"${POLICY_PAYLOAD_2_B64}\"
}
}" \
"${INSTANCE_URL}/v1beta/${POLICY_NAME}:approve" | jq
elif [ "$POLICY_STATE" == "ACTIVE" ]; then
echo "Policy '${POLICY_NAME}' is already ACTIVE. Skipping approval."
else
echo "Policy '${POLICY_NAME}' is in an unexpected state: $POLICY_STATE"
fi
比特幣
# 1. Check the policy state
POLICY_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth print-access-token "eve@example.com")" \
-H "X-Goog-User-Project: my-project" \
"${INSTANCE_URL}/v1beta/${POLICY_NAME}")
POLICY_STATE=$(echo "$POLICY_INFO" | jq -r '.state // "UNKNOWN"')
if [ "$POLICY_STATE" == "PENDING_ACTIVE" ]; then
# 2. Sign as Policy Author 2 naming transaction_policy ($POLICY_NAME)
QUORUM_SIGNATURE_2=$(sign_policy "author-eve" "transaction_policy" "${POLICY_NAME}" "ACTIVATE" "${CEDAR_RULE}")
POLICY_PAYLOAD_2_B64=$(openssl base64 -A -in policy_payload.bin)
# 3. Call ApproveTransactionPolicy as Policy Author 2
curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "eve@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d "{
\"authorizationInfo\": {
\"signature\": \"${QUORUM_SIGNATURE_2}\",
\"signedPayload\": \"${POLICY_PAYLOAD_2_B64}\"
}
}" \
"${INSTANCE_URL}/v1beta/${POLICY_NAME}:approve" | jq
elif [ "$POLICY_STATE" == "ACTIVE" ]; then
echo "Policy '${POLICY_NAME}' is already ACTIVE. Skipping approval."
else
echo "Policy '${POLICY_NAME}' is in an unexpected state: $POLICY_STATE"
fi
C. 建立及管理 KeyList (選用)
KeyList (KeyListService) 是區塊鏈位址 (manualEntry、MPC key 或 keyRing) 的具名清單,政策可將其視為可信任的目的地。管理員 (ADMIN) 可以使用 keyLists.create 建立 KeyList,並隨時使用 keyLists.addEntries 和 keyLists.removeEntries 更新項目:
curl
KEY_LIST_RESPONSE=$(curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"displayName": "Approved Destinations",
"description": "Trusted recipient addresses",
"entries": [
{
"manualEntry": {
"address": "0x1111111111111111111111111111111111111111",
"source": "Treasury onboarding",
"reason": "Approved recipient address"
}
}
]
}' \
"${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/keyLists?requestId=11111111-1111-4111-8111-111111111301")
echo "$KEY_LIST_RESPONSE" | jq
export KEY_LIST_NAME=$(echo "$KEY_LIST_RESPONSE" | jq -r '.name')
步驟 5:建立金鑰
以管理員 1 (admin-1@example.com) 的身分,建立含有內嵌 KeyPolicy 的 KeyRing,並產生分散式 MPC 金鑰。
A. 建立含有內嵌金鑰政策的金鑰環
KeyRing 是 MPC 金鑰的邏輯群組,並定義不可變更的內嵌 KeyPolicy (keyRing.keyPolicy),在 KeyRing 中建立的所有金鑰都會遵循這個政策。KeyPolicy 是門檻規則 (PolicyNode) 的樹狀結構,其子項 subKeyShares 可以是 teamNode (Team 中 totalMembers 的 threshold)、userNode (特定 SIGNER User) 或巢狀 policyNode。
我們在 ${TEAM_NAME} (threshold: 2、totalMembers: 3,儲存在 GCP_TEE) 中,為三個簽署者設定 2-of-3 門檻規則:
curl
KEY_RING_RESPONSE=$(jq -n \
--arg team "${TEAM_NAME}" \
'{
displayName: "Treasury KeyRing",
keyPolicy: {
policy: {
policyNodeId: "treasury-2-of-3",
threshold: 1,
subKeyShares: [
{
teamNode: {
team: $team,
threshold: 2,
totalMembers: 3,
storageInfo: {storageType: "GCP_TEE"}
}
}
]
}
}
}' \
| curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d @- \
"${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/keyRings?requestId=11111111-1111-4111-8111-111111111401")
echo "$KEY_RING_RESPONSE" | jq
export KEY_RING_NAME=$(echo "$KEY_RING_RESPONSE" | jq -r '.name')
echo "Created KeyRing: $KEY_RING_NAME"
B. 產生分散式金鑰 (DKG)
以管理員 1 的身分呼叫 keys.create (CreateKey),在 KeyRing 的 KeyPolicy 選取的參與者之間執行分散式金鑰產生 (DKG)。
CreateKey 是同步作業,會傳回 ACTIVE Key 資源,包括 publicKeyInfo.publicKey 和伺服器衍生的 publicKeyInfo.blockchainAddresses (以太坊和比特幣為 EC_SIGN_SECP256K1,Solana 則為 EC_SIGN_ED25519,支援後):
curl
KEY_RESPONSE=$(curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"algorithm": "EC_SIGN_SECP256K1"
}' \
"${INSTANCE_URL}/v1beta/${KEY_RING_NAME}/keys?requestId=11111111-1111-4111-8111-111111111501")
echo "$KEY_RESPONSE" | jq
export KEY_NAME=$(echo "$KEY_RESPONSE" | jq -r '.name')
export PUBLIC_KEY_B64=$(echo "$KEY_RESPONSE" | jq -r '.publicKeyInfo.publicKey')
C. 衍生區塊鏈位址並找出 UTXO
CreateKey 完成後,回應中的 publicKeyInfo.blockchainAddresses 會自動包含衍生地址 (EC_SIGN_SECP256K1 的 secp256k1Addresses.ethereumAddress 和 secp256k1Addresses.bitcoinMainnetAddresses,或 EC_SIGN_ED25519 的 ed25519Addresses.solanaAddress)。您也可以使用下列指令碼,從 65 位元組未壓縮的公開金鑰 (0x04 || X || Y) 獨立驗證或衍生 Testnet 地址和鎖定指令碼,並找出交易所需的任何未花費資金。
Ethereum
Ethereum 位址會以 .publicKeyInfo.blockchainAddresses.secp256k1Addresses.ethereumAddress 傳回,也可以從 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 publicKey from Step 5.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
為以太坊位址提供資金
在步驟 7 廣播前,請確認衍生出的以太坊地址有足夠的 Sepolia 測試網 ETH,可支付交易金額和燃料費用。只要在 Faucet for Ethereum Sepolia 輸入新產生的位址,即可索取免費的測試網 ETH。
比特幣
比特幣主網位址 (p2wpkhAddress、p2shP2wpkhAddress 和 p2pkhAddress) 會在 .publicKeyInfo.blockchainAddresses.secp256k1Addresses.bitcoinMainnetAddresses 中傳回。您也可以從未壓縮的 65 位元組 secp256k1 EC 公開金鑰 (0x04 || X || Y) 衍生出 33 位元組的壓縮公開金鑰、鎖定指令碼,以及所有三種格式的 Testnet4 位址:
- 原生隔離見證 (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 5.B (publicKeyInfo.publicKey, 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)。在步驟 6 建構交易之前:
- 為衍生出的原生隔離見證地址提供資金 (例如使用測試網水龍頭)。
- 使用比特幣節點 (
bitcoin-cli listunspent 1 9999999 "[\"tb1q...\"]") 或區塊瀏覽器 API,查詢地址未花費的 UTXO。 - 請記下以下 4 個參數,以便在步驟 6 使用:
txid:未花費輸出內容的 32 位元組交易雜湊。vout:以零為索引的輸出索引 (例如0或1)。value:未花費的聰數 (例如200000聰 = 0.002 BTC)。scriptPubKey:鎖定指令碼十六進位值 (例如0014...)。
步驟 6:執行 MPC 作業
當 SIGNER 呼叫 transactions.create (CreateTransaction) 時,系統會同時評估政策並啟動簽署程序。執行個體會根據 schema 解碼 payload、評估所有有效的交易政策、將結果記錄在 policyEvaluationRecord,並記錄啟動簽署者的 AUTHORIZE 決定。
A. 建立交易 (評估政策並啟動簽署程序)
以簽署者 1 (alice@example.com) 的身分提交交易,進行政策評估和簽署:
Ethereum、KMS
🛑 情況 A:拒絕 (價值過高)
嘗試建立 3 ETH 的交易 (超過 2 ETH 的政策限制):
sign_tx_decision() {
local signer_id=$1
local resource_type=$2
local resource_name=$3
local decision=$4
local digest_hex=$5
python3 -c 'import sys; rt, rn, dec, dg = sys.argv[1:]; f = lambda k, b: bytes([k << 3 | 2, len(b)]) + b; open("tx_decision_payload.bin", "wb").write(f(1 if rt == "key" else 2, rn.encode()) + bytes([24, 1 if dec == "AUTHORIZE" else 2]) + f(4, bytes.fromhex(dg)))' "${resource_type}" "${resource_name}" "${decision}" "${digest_hex}"
gcloud kms asymmetric-sign \
--version=1 \
--key="${signer_id}-key" \
--keyring="mpc-assent-keys" \
--location="us-central1" \
--digest-algorithm=sha256 \
--input-file=tx_decision_payload.bin \
--signature-file=tx_decision_sig.bin > /dev/null
openssl base64 -A -in tx_decision_sig.bin
}
# Payload for 3 ETH (amount_token_scaled = 3,000,000,000 > 2,000,000,000)
RAW_TX_BYTES_HEX="ef808504a817c8008252089411111111111111111111111111111111111111118829a2241af62c00008083aa36a78080"
PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$RAW_TX_BYTES_HEX')).decode('utf-8'))")
DIGEST_HEX=$(python3 -c "from eth_utils import keccak; print(keccak(bytes.fromhex('$RAW_TX_BYTES_HEX')).hex())")
DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$DIGEST_HEX')).decode('utf-8'))")
SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)
curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d "{
\"transaction\": {
\"action\": \"Action::\\\"Transfer\\\"\",
\"schema\": \"ETHEREUM_TX_V2\",
\"payload\": \"${PAYLOAD_B64}\",
\"digest\": \"${DIGEST_B64}\"
},
\"authorizationInfo\": {
\"signature\": \"${SIGNER_1_SIG_B64}\",
\"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
},
\"requestId\": \"11111111-1111-4111-8111-111111111601\"
}" \
"${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions" | jq
預期的政策評估結果:decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED)。
✅ 案例 B:核准 (允許值)
建立 1 Gwei 的交易 (在 2 ETH 限制內):
# Payload for 1 Gwei
RAW_TX_BYTES_HEX="eb808504a817c800825208941111111111111111111111111111111111111111843b9aca008083aa36a78080"
PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$RAW_TX_BYTES_HEX')).decode('utf-8'))")
export DIGEST_HEX=$(python3 -c "from eth_utils import keccak; print(keccak(bytes.fromhex('$RAW_TX_BYTES_HEX')).hex())")
export DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$DIGEST_HEX')).decode('utf-8'))")
SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)
RESPONSE=$(curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d "{
\"transaction\": {
\"action\": \"Action::\\\"Transfer\\\"\",
\"schema\": \"ETHEREUM_TX_V2\",
\"payload\": \"${PAYLOAD_B64}\",
\"digest\": \"${DIGEST_B64}\"
},
\"authorizationInfo\": {
\"signature\": \"${SIGNER_1_SIG_B64}\",
\"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
},
\"requestId\": \"11111111-1111-4111-8111-111111111602\"
}" \
"${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions")
echo "$RESPONSE" | jq
export TRANSACTION_NAME=$(echo "$RESPONSE" | jq -r '.name')
預期的回應:state: PENDING_AUTHORIZATION 和 policyEvaluationRecord.decision: PERMIT。
Ethereum、OpenSSL
🛑 情況 A:拒絕 (價值過高)
嘗試建立 3 ETH 的交易 (超過 2 ETH 的政策限制):
sign_tx_decision() {
local signer_id=$1
local resource_type=$2
local resource_name=$3
local decision=$4
local digest_hex=$5
python3 -c 'import sys; rt, rn, dec, dg = sys.argv[1:]; f = lambda k, b: bytes([k << 3 | 2, len(b)]) + b; open("tx_decision_payload.bin", "wb").write(f(1 if rt == "key" else 2, rn.encode()) + bytes([24, 1 if dec == "AUTHORIZE" else 2]) + f(4, bytes.fromhex(dg)))' "${resource_type}" "${resource_name}" "${decision}" "${digest_hex}"
openssl dgst -sha256 -sign "${signer_id}_priv.pem" -out tx_decision_sig.bin tx_decision_payload.bin
openssl base64 -A -in tx_decision_sig.bin
}
# Payload for 3 ETH (amount_token_scaled = 3,000,000,000 > 2,000,000,000)
RAW_TX_BYTES_HEX="ef808504a817c8008252089411111111111111111111111111111111111111118829a2241af62c00008083aa36a78080"
PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$RAW_TX_BYTES_HEX')).decode('utf-8'))")
DIGEST_HEX=$(python3 -c "from eth_utils import keccak; print(keccak(bytes.fromhex('$RAW_TX_BYTES_HEX')).hex())")
DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$DIGEST_HEX')).decode('utf-8'))")
SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)
curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d "{
\"transaction\": {
\"action\": \"Action::\\\"Transfer\\\"\",
\"schema\": \"ETHEREUM_TX_V2\",
\"payload\": \"${PAYLOAD_B64}\",
\"digest\": \"${DIGEST_B64}\"
},
\"authorizationInfo\": {
\"signature\": \"${SIGNER_1_SIG_B64}\",
\"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
},
\"requestId\": \"11111111-1111-4111-8111-111111111601\"
}" \
"${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions" | jq
預期的政策評估結果:decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED)。
✅ 案例 B:核准 (允許值)
建立 1 Gwei 的交易 (在 2 ETH 限制內):
# Payload for 1 Gwei
RAW_TX_BYTES_HEX="eb808504a817c800825208941111111111111111111111111111111111111111843b9aca008083aa36a78080"
PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$RAW_TX_BYTES_HEX')).decode('utf-8'))")
export DIGEST_HEX=$(python3 -c "from eth_utils import keccak; print(keccak(bytes.fromhex('$RAW_TX_BYTES_HEX')).hex())")
export DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$DIGEST_HEX')).decode('utf-8'))")
SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)
RESPONSE=$(curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d "{
\"transaction\": {
\"action\": \"Action::\\\"Transfer\\\"\",
\"schema\": \"ETHEREUM_TX_V2\",
\"payload\": \"${PAYLOAD_B64}\",
\"digest\": \"${DIGEST_B64}\"
},
\"authorizationInfo\": {
\"signature\": \"${SIGNER_1_SIG_B64}\",
\"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
},
\"requestId\": \"11111111-1111-4111-8111-111111111602\"
}" \
"${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions")
echo "$RESPONSE" | jq
export TRANSACTION_NAME=$(echo "$RESPONSE" | jq -r '.name')
預期的回應:state: PENDING_AUTHORIZATION 和 policyEvaluationRecord.decision: PERMIT。
Bitcoin、KMS
在比特幣 UTXO 模型中,交易會花費特定的未花費輸出。在 SegWit (BIP 143) 中,計算簽章雜湊時,必須知道 satoshi 數量和所花費輸入內容的鎖定指令碼。
比特幣政策評估作業必須採用 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 5.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 政策評估決策。
sign_tx_decision() {
local signer_id=$1
local resource_type=$2
local resource_name=$3
local decision=$4
local digest_hex=$5
python3 -c 'import sys; rt, rn, dec, dg = sys.argv[1:]; f = lambda k, b: bytes([k << 3 | 2, len(b)]) + b; open("tx_decision_payload.bin", "wb").write(f(1 if rt == "key" else 2, rn.encode()) + bytes([24, 1 if dec == "AUTHORIZE" else 2]) + f(4, bytes.fromhex(dg)))' "${resource_type}" "${resource_name}" "${decision}" "${digest_hex}"
gcloud kms asymmetric-sign \
--version=1 \
--key="${signer_id}-key" \
--keyring="mpc-assent-keys" \
--location="us-central1" \
--digest-algorithm=sha256 \
--input-file=tx_decision_payload.bin \
--signature-file=tx_decision_sig.bin > /dev/null
openssl base64 -A -in tx_decision_sig.bin
}
# Construct a PSBT payload with miner_fee_sats = 15000 -> max_fee_atomic = 150000 (exceeds 100000 limit)
REJECTED_PSBT_HEX="70736274ff0100710200000001d81639a6635e3921442629bc7f51f22ca9e4ca3d27c280c9f1936915334f598f0100000000ffffffff0250c3000000000000160014751e76e8199196d454941c45d1b3a323f1433bd6580f020000000000160014eced05cd51d964102a20c856b09080ad620e9da2000000000001011f400d030000000000160014eced05cd51d964102a20c856b09080ad620e9da2000000"
REJECTED_PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$REJECTED_PSBT_HEX')).decode('utf-8'))")
REJECTED_DIGEST_HEX=$(python3 -c "from embit import script; from embit.psbt import PSBT; p=PSBT.parse(bytes.fromhex('$REJECTED_PSBT_HEX')); u=p.inputs[0].witness_utxo; sc=script.Script(b'\x76\xa9\x14'+u.script_pubkey.data[2:]+b'\x88\xac'); print(p.tx.sighash_segwit(0, sc, u.value).hex())")
REJECTED_DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$REJECTED_DIGEST_HEX')).decode('utf-8'))")
SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${REJECTED_DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)
curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d "{
\"transaction\": {
\"action\": \"Action::\\\"Transfer\\\"\",
\"schema\": \"BITCOIN_TX_V2\",
\"payload\": \"${REJECTED_PAYLOAD_B64}\",
\"digest\": \"${REJECTED_DIGEST_B64}\"
},
\"authorizationInfo\": {
\"signature\": \"${SIGNER_1_SIG_B64}\",
\"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
},
\"requestId\": \"11111111-1111-4111-8111-111111111603\"
}" \
"${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions" | jq
預期的政策評估結果:decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED)。
✅ 案例 B:核准 (允許比特幣 PSBT)
使用先前建構的有效 PSBT 建立交易 (RECIPIENT_SATS = 50000 對應至 amount_token_scaled = 500000 <= 2500000,MINER_FEE_SATS = 250 對應至 max_fee_atomic = 2500 <= 100000):
PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$BTC_PSBT_HEX')).decode('utf-8'))")
export DIGEST_HEX=$(python3 -c "from embit import script; from embit.psbt import PSBT; p=PSBT.parse(bytes.fromhex('$BTC_PSBT_HEX')); u=p.inputs[0].witness_utxo; sc=script.Script(b'\x76\xa9\x14'+u.script_pubkey.data[2:]+b'\x88\xac'); print(p.tx.sighash_segwit(0, sc, u.value).hex())")
export DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$DIGEST_HEX')).decode('utf-8'))")
SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)
RESPONSE=$(curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d "{
\"transaction\": {
\"action\": \"Action::\\\"Transfer\\\"\",
\"schema\": \"BITCOIN_TX_V2\",
\"payload\": \"${PAYLOAD_B64}\",
\"digest\": \"${DIGEST_B64}\"
},
\"authorizationInfo\": {
\"signature\": \"${SIGNER_1_SIG_B64}\",
\"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
},
\"requestId\": \"11111111-1111-4111-8111-111111111604\"
}" \
"${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions")
echo "$RESPONSE" | jq
export TRANSACTION_NAME=$(echo "$RESPONSE" | jq -r '.name')
預期的回應:state: PENDING_AUTHORIZATION 和 policyEvaluationRecord.decision: PERMIT。
Bitcoin、OpenSSL
在比特幣 UTXO 模型中,交易會花費特定的未花費輸出。在 SegWit (BIP 143) 中,計算簽章雜湊時,必須知道 satoshi 數量和所花費輸入內容的鎖定指令碼。
比特幣政策評估作業必須採用 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 5.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 政策評估決策。
sign_tx_decision() {
local signer_id=$1
local resource_type=$2
local resource_name=$3
local decision=$4
local digest_hex=$5
python3 -c 'import sys; rt, rn, dec, dg = sys.argv[1:]; f = lambda k, b: bytes([k << 3 | 2, len(b)]) + b; open("tx_decision_payload.bin", "wb").write(f(1 if rt == "key" else 2, rn.encode()) + bytes([24, 1 if dec == "AUTHORIZE" else 2]) + f(4, bytes.fromhex(dg)))' "${resource_type}" "${resource_name}" "${decision}" "${digest_hex}"
openssl dgst -sha256 -sign "${signer_id}_priv.pem" -out tx_decision_sig.bin tx_decision_payload.bin
openssl base64 -A -in tx_decision_sig.bin
}
# Construct a PSBT payload with miner_fee_sats = 15000 -> max_fee_atomic = 150000 (exceeds 100000 limit)
REJECTED_PSBT_HEX="70736274ff0100710200000001d81639a6635e3921442629bc7f51f22ca9e4ca3d27c280c9f1936915334f598f0100000000ffffffff0250c3000000000000160014751e76e8199196d454941c45d1b3a323f1433bd6580f020000000000160014eced05cd51d964102a20c856b09080ad620e9da2000000000001011f400d030000000000160014eced05cd51d964102a20c856b09080ad620e9da2000000"
REJECTED_PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$REJECTED_PSBT_HEX')).decode('utf-8'))")
REJECTED_DIGEST_HEX=$(python3 -c "from embit import script; from embit.psbt import PSBT; p=PSBT.parse(bytes.fromhex('$REJECTED_PSBT_HEX')); u=p.inputs[0].witness_utxo; sc=script.Script(b'\x76\xa9\x14'+u.script_pubkey.data[2:]+b'\x88\xac'); print(p.tx.sighash_segwit(0, sc, u.value).hex())")
REJECTED_DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$REJECTED_DIGEST_HEX')).decode('utf-8'))")
SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${REJECTED_DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)
curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d "{
\"transaction\": {
\"action\": \"Action::\\\"Transfer\\\"\",
\"schema\": \"BITCOIN_TX_V2\",
\"payload\": \"${REJECTED_PAYLOAD_B64}\",
\"digest\": \"${REJECTED_DIGEST_B64}\"
},
\"authorizationInfo\": {
\"signature\": \"${SIGNER_1_SIG_B64}\",
\"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
},
\"requestId\": \"11111111-1111-4111-8111-111111111603\"
}" \
"${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions" | jq
預期的政策評估結果:decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED)。
✅ 案例 B:核准 (允許比特幣 PSBT)
使用先前建構的有效 PSBT 建立交易 (RECIPIENT_SATS = 50000 對應至 amount_token_scaled = 500000 <= 2500000,MINER_FEE_SATS = 250 對應至 max_fee_atomic = 2500 <= 100000):
PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$BTC_PSBT_HEX')).decode('utf-8'))")
export DIGEST_HEX=$(python3 -c "from embit import script; from embit.psbt import PSBT; p=PSBT.parse(bytes.fromhex('$BTC_PSBT_HEX')); u=p.inputs[0].witness_utxo; sc=script.Script(b'\x76\xa9\x14'+u.script_pubkey.data[2:]+b'\x88\xac'); print(p.tx.sighash_segwit(0, sc, u.value).hex())")
export DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$DIGEST_HEX')).decode('utf-8'))")
SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)
RESPONSE=$(curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d "{
\"transaction\": {
\"action\": \"Action::\\\"Transfer\\\"\",
\"schema\": \"BITCOIN_TX_V2\",
\"payload\": \"${PAYLOAD_B64}\",
\"digest\": \"${DIGEST_B64}\"
},
\"authorizationInfo\": {
\"signature\": \"${SIGNER_1_SIG_B64}\",
\"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
},
\"requestId\": \"11111111-1111-4111-8111-111111111604\"
}" \
"${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions")
echo "$RESPONSE" | jq
export TRANSACTION_NAME=$(echo "$RESPONSE" | jq -r '.name')
預期的回應:state: PENDING_AUTHORIZATION 和 policyEvaluationRecord.decision: PERMIT。
🕵️♂️ 檢查交易狀態 (輪詢)
定期執行下列指令,輪詢交易狀態:
curl
curl -s -X GET \
-H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
-H "X-Goog-User-Project: my-project" \
"${INSTANCE_URL}/v1beta/${TRANSACTION_NAME}" | jq
B. 記錄簽署者決定 (授權)
由於步驟 6.A 中的 CreateTransaction 已記錄簽署者 1 的 AUTHORIZE 決策 (3 分之 2 KeyPolicy 門檻所需的 2 個簽署者之一),因此只需要另一位持有金鑰共用權的共同簽署者,使用 transactions.recordSignerDecision (RecordSignerDecision) 授權交易即可。
簽署 TransactionSigningPayload,將 transaction (${TRANSACTION_NAME}) 命名為 recordSignerDecision,並以簽署人 2 (bob@example.com) 的身分呼叫:
curl
# 1. Check state for Signer 2
TX_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth print-access-token "bob@example.com")" \
-H "X-Goog-User-Project: my-project" \
"${INSTANCE_URL}/v1beta/${TRANSACTION_NAME}")
TX_STATE=$(echo "$TX_INFO" | jq -r '.state // "UNKNOWN"')
if [ "$TX_STATE" == "PENDING_AUTHORIZATION" ]; then
# 2. Sign as Signer 2 naming transaction ($TRANSACTION_NAME)
SIGNER_2_SIG_B64=$(sign_tx_decision "signer-bob" "transaction" "${TRANSACTION_NAME}" "AUTHORIZE" "${DIGEST_HEX}")
SIGNER_2_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)
# 3. Call RecordSignerDecision as Signer 2
echo "Recording AUTHORIZE decision for Signer 2..."
curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "bob@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d "{
\"decision\": \"AUTHORIZE\",
\"authorizationInfo\": {
\"signature\": \"${SIGNER_2_SIG_B64}\",
\"signedPayload\": \"${SIGNER_2_PAYLOAD_B64}\"
}
}" \
"${INSTANCE_URL}/v1beta/${TRANSACTION_NAME}:recordSignerDecision" | jq
elif [ "$TX_STATE" == "SIGNING" ] || [ "$TX_STATE" == "SIGNING_SUCCEEDED" ]; then
echo "Transaction is in state $TX_STATE. Skipping Signer 2."
else
echo "Transaction is in an unexpected state: $TX_STATE"
fi
C. 擷取最終簽名
達到授權門檻後,請檢查交易狀態並擷取 finalSignature (如果是 EC_SIGN_SECP256K1 金鑰,則為 recoveryId):
curl
curl -s -X GET \
-H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
-H "X-Goog-User-Project: my-project" \
"${INSTANCE_URL}/v1beta/${TRANSACTION_NAME}" | jq
如果 state 為 SIGNING_SUCCEEDED,請從傳回的 JSON 複製 finalSignature 欄位,用於步驟 7。
步驟 7:將交易廣播至區塊鏈
交易達到 SIGNING_SUCCEEDED 狀態,且您已擷取 finalSignature 後,最後一個步驟是建構已簽署的線路交易,並廣播至網路。
Ethereum
復原 ID 衍生和廣播
MPC API 會在 finalSignature 中傳回原始 64 位元組 ECDSA 簽章 (包含標準低 S 形式的 32 位元組 r 和 s 值,符合 EIP-2 標準),以及 recoveryId 中的復原同位位元 (0 或 1)。
下列 Python 指令碼會使用 finalSignature 建構已簽署的交易、根據 MPC 金鑰的 Ethereum 位址驗證復原的寄件者位址,並將交易廣播至網路。
廣播腳本:
在終端機中執行下列可複製貼上的區塊,編寫並執行 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欄位與步驟 6 處理的內容相符。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. 執行個體
檢查及管理專案和位置中的 MPC 執行個體。
A. 列出所有執行個體
curl
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
"https://mpc.googleapis.com/v1beta/projects/my-project/locations/us-central1/instances" | jq
B. 取得特定執行個體的詳細資料
curl
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
"https://mpc.googleapis.com/v1beta/${INSTANCE_NAME}" | jq
👥 2. 團隊與使用者
檢查執行個體中的團隊和使用者。
A. 列出所有團隊
curl
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
"${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/teams" | jq
B. 取得特定團隊的詳細資料
curl
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
"${INSTANCE_URL}/v1beta/${TEAM_NAME}" | jq
C. 列出所有已註冊的使用者
curl
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
"${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/users" | jq
D. 取得特定使用者的詳細資料
curl
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
"${INSTANCE_URL}/v1beta/${SIGNER_1_NAME}" | jq
📜 3. 交易政策和 KeyList
檢查交易政策和金鑰清單。
A. 列出所有交易政策
curl
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
"${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/transactionPolicies" | jq
B. 取得特定交易政策的詳細資料
curl
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
"${INSTANCE_URL}/v1beta/${POLICY_NAME}" | jq
C. 列出及取得 KeyList
curl
# List all KeyLists
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
"${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/keyLists" | jq
# Get a specific KeyList
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
"${INSTANCE_URL}/v1beta/${KEY_LIST_NAME}" | jq
🔑 4. 金鑰和金鑰環
檢查金鑰環和分散式金鑰。
A. 列出所有 KeyRing
curl
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
"${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/keyRings" | jq
B. 列出金鑰環中的所有金鑰
curl
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
"${INSTANCE_URL}/v1beta/${KEY_RING_NAME}/keys" | jq
C. 取得特定分散式金鑰的詳細資料
curl
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
"${INSTANCE_URL}/v1beta/${KEY_NAME}" | jq
📝 5. 交易 (簽署作業)
檢查交易和簽署狀態。
A. 列出特定金鑰 (或所有金鑰) 的所有交易
curl
# List transactions for a specific Key
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
"${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions" | jq
# List transactions across all KeyRings and Keys using the '-' wildcard
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
"${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/keyRings/-/keys/-/transactions" | jq
B. 取得特定交易的詳細資料或狀態
curl
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
-H "X-Goog-User-Project: my-project" \
"${INSTANCE_URL}/v1beta/${TRANSACTION_NAME}" | jq