本指南介绍了如何使用 Multi-party Compute API 管理分布式密钥、参与者和政策。它适用于学习关键用户体验历程的预览版客户。
概览
MPC API 包含两个主要组件:
- Core MPC 管理分布式密钥、提案和签名仪式的生命周期。
- 政策引擎负责强制执行治理,在操作执行前进行授权。
如需详细了解 API 访问权限和加密签名如何交互,请参阅安全和授权。
前提条件
开始之前,请确保您满足以下方面的要求:
- 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 中的外部同意密钥。 |
| Signer 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 步:生成同意密钥
如需使用 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),并创建一条规则来描述允许执行的操作。虽然您可以配置多项政策来管理不同的场景,但本指南仅演示如何为以太坊或比特币交易设置政策。默认情况下,政策引擎会快速失败(默认屏蔽);如果没有任何政策明确允许某项交易,该交易就会被拒绝。
在创建或批准政策之前,作者必须对表示意图的载荷进行签名。您可以使用以下辅助函数来处理二进制序列化和签名。
以太坊
此规则允许向 Alice 的特定地址进行较小金额的以太坊转账(<= 2 ETH,表示为 2,000,000,000 Gwei)。
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. 批准政策(转换为 ACTIVE)
如果您的政策设置需要 M-of-N 签名,则后续作者会调用 AddQuorumSignature。
如需生成签名,您必须先检索政策的当前序列。
以太坊
# 1. Get the policy state and sequence
POLICY_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/ethereum-transfer-policy")
POLICY_STATE=$(echo "$POLICY_INFO" | jq -r '.state // "UNKNOWN"')
if [ "$POLICY_STATE" == "PENDING_ACTIVE" ]; then
POLICY_SEQUENCE=$(echo "$POLICY_INFO" | jq -r '.globalSequence // 0')
echo "Policy Sequence: $POLICY_SEQUENCE"
# 2. Sign as Author 2
QUORUM_SIGNATURE_SEQ=$(sign_policy "author-eve" $POLICY_SEQUENCE 'permit(principal == User::"alice", action == Action::"Transfer", resource == Resource::"mpc_operation") when { context.transaction.to_address == "0x1111111111111111111111111111111111111111" && context.transaction.value_gwei <= 2000000000 };' "ethereum-transfer-policy")
# 3. Call AddQuorumSignature
curl -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"name": "projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/ethereum-transfer-policy",
"signature": {
"authorId": "'"author-eve"'",
"signature": "'"$QUORUM_SIGNATURE_SEQ"'",
"approvalTime": "'"$(date -u +"%Y-%m-%dT%H:%M:%SZ")"'"
}
}' \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/ethereum-transfer-policy:addQuorumSignature" | jq
elif [ "$POLICY_STATE" == "ACTIVE" ]; then
echo "Policy 'ethereum-transfer-policy' is already ACTIVE. Skipping signature."
else
echo "Policy 'ethereum-transfer-policy' is in an unknown or unexpected state: $POLICY_STATE"
fi
比特币
# 1. Get the policy state and sequence
POLICY_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/bitcoin-psbt-policy")
POLICY_STATE=$(echo "$POLICY_INFO" | jq -r '.state // "UNKNOWN"')
if [ "$POLICY_STATE" == "PENDING_ACTIVE" ]; then
POLICY_SEQUENCE=$(echo "$POLICY_INFO" | jq -r '.globalSequence // 0')
echo "Policy Sequence: $POLICY_SEQUENCE"
# 2. Sign as Author 2
QUORUM_SIGNATURE_SEQ=$(sign_policy "author-eve" $POLICY_SEQUENCE 'permit(principal == User::"alice", action == Action::"Transfer", resource == Resource::"mpc_operation") when { context.transaction.schema == "bitcoin.transaction.v2" && context.transaction.spend_amount_scaled <= 2500000 && context.transaction.max_fee_atomic <= 100000 };' "bitcoin-psbt-policy")
# 3. Call AddQuorumSignature
curl -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"name": "projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/bitcoin-psbt-policy",
"signature": {
"authorId": "'"author-eve"'",
"signature": "'"$QUORUM_SIGNATURE_SEQ"'",
"approvalTime": "'"$(date -u +"%Y-%m-%dT%H:%M:%SZ")"'"
}
}' \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/bitcoin-psbt-policy:addQuorumSignature" | jq
elif [ "$POLICY_STATE" == "ACTIVE" ]; then
echo "Policy 'bitcoin-psbt-policy' is already ACTIVE. Skipping signature."
else
echo "Policy 'bitcoin-psbt-policy' is in an unknown or unexpected state: $POLICY_STATE"
fi
第 4 步:创建密钥
现在,我们创建一个密钥环并生成一个分布式密钥。
A. 创建密钥环
MPC 密钥的逻辑分组。
curl
# Check existence
if curl -s -f -o /dev/null -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring"; then
echo "KeyRing 'my-key-ring' already exists. Skipping creation."
else
echo "KeyRing 'my-key-ring' not found. Creating..."
curl -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{}' \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings?key_ring_id=my-key-ring | jq
fi
B. 生成分布式密钥 (DKG)
发起异步仪式,在参与者之间生成分布式密钥。
curl
# Check existence
if curl -s -f -o /dev/null -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key"; then
echo "Key 'my-key' already exists. Skipping generation."
else
echo "Key 'my-key' not found. Generating..."
curl -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"threshold": 2,
"algorithm": "ECDSA_SECP256K1",
"participantShares": [
{"participantId": "projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/signers/signer-alice"},
{"participantId": "projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/signers/signer-bob"},
{"participantId": "projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/signers/signer-charlie"}
]
}' \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys?key_id=my-key | jq
fi
此操作会返回一个长时间运行的操作 (LRO)。您必须等待密钥 state 变为 ACTIVE(这意味着 DKG 仪式已成功)。
🕵️♂️ 检查按键状态(轮询)
定期运行以下命令来轮询密钥状态,直到响应中的 state 字段变为 ACTIVE(表示 DKG 仪式已成功)。
curl
curl -X GET \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key | jq
C. 派生区块链地址并定位 UTXO
当分布式密钥变为 ACTIVE 后,MPC 密钥即可使用。在此步骤中,您将从 MPC 密钥生成 (0x04 || X || Y) 返回的 65 字节未压缩公钥中派生出区块链专用地址,并找到交易所需的任何未花费资金。
以太坊
以太坊地址派生自 64 字节未压缩公钥点 (pub_bytes[1:]) 的 Keccak-256 哈希,取最后 20 个字节。
地址派生脚本 (derive_address.py)
执行以下代码块,以创建并运行 derive_address.py:
cat <<'EOF' > derive_address.py
import base64
from eth_utils import keccak, to_checksum_address
# Insert your base64 public_key from Step 4.B
PUBLIC_KEY_B64 = "your-public-key"
pub_bytes = base64.b64decode(PUBLIC_KEY_B64)
if len(pub_bytes) == 65 and pub_bytes[0] == 4:
raw_addr = "0x" + keccak(pub_bytes[1:])[-20:].hex()
checksummed_addr = to_checksum_address(raw_addr)
print("Ethereum Address:", checksummed_addr)
else:
print(f"Error: Expected 65-byte uncompressed key, got {len(pub_bytes)} bytes.")
EOF
python3 derive_address.py
为以太坊地址充值
在第 6 步中广播之前,请确保您的派生以太坊地址有足够的 Sepolia 测试网 ETH 来支付交易价值和 gas 费用。您可以将新派生的地址输入到 Ethereum Sepolia 水龙头中,申请免费的测试网 ETH。
比特币
比特币地址派生自未压缩的 65 字节 secp256k1 EC 公钥 (0x04 || X || Y)。同一公钥支持多种地址格式:
- 原生隔离见证 (P2WPKH / BIP 173):Bech32 地址(主网上的
bc1q...,Testnet4 上的tb1q...),具有 22 字节的见证scriptPubKey(0014<hash160>)。这是推荐的现代格式,交易费用最低。 - 嵌套隔离见证 (P2SH-P2WPKH / BIP 49):封装的隔离见证地址(主网上的
3...,Testnet4 上的2...),具有 23 字节的scriptPubKey(a914<hash160>87) 和 22 字节的 redeemScript (0014<hash160>),与旧系统兼容。 - Legacy (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。 - 请注意第 5 步中的以下 4 个参数:
txid:未花费输出的 32 字节交易哈希。vout:从零开始的输出索引(例如0或1)。value:未花费的金额,以聪为单位(例如,200000聪 = 0.002 BTC)。scriptPubKey:锁定脚本十六进制(例如0014...)。
第 5 步:执行 MPC 操作
通过评估交易并创建提案来执行 MPC 操作。
A. 评估交易(获取授权令牌)
在对 MPC 操作进行签名之前,请根据您的政策对载荷进行授权,以接收 JWS 令牌和 32 字节的签名摘要。
以太坊
🛑 案例 A:拒绝(价值过高)
尝试授权一笔 3 ETH 的交易(超出 2 ETH 的限额)。
# Payload for 3 ETH (Hex equivalent contains value_gwei > 2B)
RAW_TX_BYTES_HEX="ef808504a817c8008252089411111111111111111111111111111111111111118829a2241af62c00008083aa36a78080"
curl -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"principal": "User::\"alice\"",
"action": "Action::\"Transfer\"",
"resource": "Resource::\"mpc_operation\"",
"schema": "ETHEREUM_TX_V1",
"payload": "'"$RAW_TX_BYTES_HEX"'"
}' \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine:authorizePayload | jq
预期响应:decision: DENY
✅ 情况 B:批准(允许的值)
尝试授权一笔 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"
🛑 Case A: Rejection (Fee Limit Exceeded)
尝试授权一笔矿工费超过 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)。预期的 Proposal state 将为 PENDING_ASSENT(表示已准备好供参与者签署)。
🕵️♂️ 检查提案状态(轮询)
定期运行以下命令,轮询提案状态。
curl
curl -X GET \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals/my-proposal | jq
C. 记录参与者的决定(同意)
参与者必须使用其本地私钥(密钥共享)对提案哈希进行签名,并转发同意。
1. 签署提案摘要
Cloud KMS
sign_digest() {
local signer_id=$1
local digest_hex=$2
local digest_b64=$(python3 -c "import base64, sys; print(base64.b64encode(bytes.fromhex('$digest_hex')).decode('utf-8'))")
# Direct API calls are used here because the input is already a pre-computed 32-byte digest from the MPC.
# Tools like gcloud assume input is raw data and apply a redundant hashing layer, altering the payload.
# Unlike Policy signings which hash raw administrative text, Proposal signatures must sign this exact 32-byte hash as-is.
curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "Content-Type: application/json" \
-d "{\"digest\": {\"sha256\": \"$digest_b64\"}}" \
https://cloudkms.googleapis.com/v1/projects/my-project/locations/us-central1/keyRings/mpc-assent-keys/cryptoKeys/${signer_id}-key/cryptoKeyVersions/1:asymmetricSign | jq -r .signature
}
SIGNER_2_SIG_B64=$(sign_digest "signer-bob" "$DIGEST_HEX")
SIGNER_3_SIG_B64=$(sign_digest "signer-charlie" "$DIGEST_HEX")
OpenSSL
sign_digest() {
local signer_id=$1
local digest_hex=$2
python3 -c "import sys; sys.stdout.buffer.write(bytes.fromhex('$digest_hex'))" > digest.bin
openssl pkeyutl -sign -inkey "${signer_id}_priv.pem" -in digest.bin -out digest_sig.bin
openssl base64 -A -in digest_sig.bin
}
SIGNER_2_SIG_B64=$(sign_digest "signer-bob" "$DIGEST_HEX")
SIGNER_3_SIG_B64=$(sign_digest "signer-charlie" "$DIGEST_HEX")
2. 记录决策
curl
# 1. Check state for Signer 2
PROPOSAL_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals/my-proposal")
PROPOSAL_STATE=$(echo "$PROPOSAL_INFO" | jq -r '.state // "UNKNOWN"')
if [ "$PROPOSAL_STATE" == "PENDING_ASSENT" ]; then
echo "Recording assent for Signer 2..."
curl -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"signature": "'"$SIGNER_2_SIG_B64"'",
"decision": "ASSENT",
"participant": "signer-bob"
}' \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals/my-proposal:recordParticipantDecision | jq
elif [ "$PROPOSAL_STATE" == "READY" ] || [ "$PROPOSAL_STATE" == "SUCCEEDED" ]; then
echo "Proposal is in state $PROPOSAL_STATE. Skipping Signer 2."
else
echo "Proposal is in an unknown or unexpected state: $PROPOSAL_STATE"
fi
# 2. Re-check state for Signer 3 (Signer 2 might have triggered a state change)
PROPOSAL_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
"https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals/my-proposal")
PROPOSAL_STATE=$(echo "$PROPOSAL_INFO" | jq -r '.state // "UNKNOWN"')
if [ "$PROPOSAL_STATE" == "PENDING_ASSENT" ]; then
echo "Recording assent for Signer 3..."
curl -X POST \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d '{
"signature": "'"$SIGNER_3_SIG_B64"'",
"decision": "ASSENT",
"participant": "signer-charlie"
}' \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals/my-proposal:recordParticipantDecision | jq
elif [ "$PROPOSAL_STATE" == "READY" ] || [ "$PROPOSAL_STATE" == "SUCCEEDED" ]; then
echo "Proposal is in state $PROPOSAL_STATE. Skipping Signer 3."
else
echo "Proposal is in an unknown or unexpected state: $PROPOSAL_STATE"
fi
D. 提取最终签名
达到同意阈值后,请检查提案状态或获取签名。
curl
curl -X GET \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals/my-proposal | jq
如果提案处于 SUCCEEDED 状态,请查看返回的 JSON 中的 finalSignature 字段。
第 6 步:将交易广播到区块链
当您的提案达到 SUCCEEDED 状态并且您已提取 finalSignature 后,最后一步是构建已签名的网络交易并将其广播到网络。
以太坊
恢复标识符派生和广播
MPC API 会返回原始 64 字节 ECDSA 签名(包含 r 和 s 值)。服务器会确保签名采用规范的 low-s 形式 (EIP-2)。不过,以太坊需要恢复标识符 (v) 来派生发送者的地址并验证签名,而标准的 64 字节 MPC 签名不会直接返回该标识符。
以下 Python 脚本可处理这种复杂情况。它会通过匹配您预期的发件人地址来动态确定正确的恢复位,构建有效的交易,并将其广播到网络。
广播脚本:
在终端中执行以下可复制粘贴的代码块,以编写并运行 broadcast_eth.py:
cat <<'EOF' > broadcast_eth.py
"""Broadcasts an Ethereum transaction using a normalized MPC signature."""
import base64
import os
import requests
from eth_account import Account
try:
from eth_account._utils.transactions import encode_transaction, serializable_unsigned_transaction_from_dict
except ImportError:
from eth_account._utils.legacy_transactions import encode_transaction, serializable_unsigned_transaction_from_dict
# ==========================================
# 1. INPUTS
# ==========================================
FINAL_SIGNATURE_B64 = "your-final-signature"
RPC_URL = "https://ethereum-sepolia-rpc.publicnode.com"
EXPECTED_SENDER = "0xyour-mpc-key-address"
if not FINAL_SIGNATURE_B64 or FINAL_SIGNATURE_B64 == "your-final-signature":
raise ValueError("FINAL_SIGNATURE_B64 is not set. Please provide a valid signature.")
# ==========================================
# 2. TRANSACTION PARAMETERS
# ==========================================
CHAIN_ID = 11155111 # Sepolia Testnet
tx_dict = {
"nonce": int("0"),
"gasPrice": int("20000000000"), # 20 Gwei
"gas": int("21000"),
"to": "0x1111111111111111111111111111111111111111",
"value": int("1000000000"), # 1 Gwei
"data": b"",
"chainId": CHAIN_ID,
}
# ==========================================
# 3. DECODE & SPLIT SIGNATURE
# ==========================================
missing_padding = len(FINAL_SIGNATURE_B64) % 4
if missing_padding:
FINAL_SIGNATURE_B64 += "=" * (4 - missing_padding)
sig_bytes = base64.b64decode(FINAL_SIGNATURE_B64)
r = int.from_bytes(sig_bytes[:32], byteorder="big")
s = int.from_bytes(sig_bytes[32:], byteorder="big")
# ==========================================
# 4. RECOVERY IDENTIFIER (V) DERIVATION
# ==========================================
chain_v_base = (CHAIN_ID * 2) + 35
# ==========================================
# 5. GENERATE UNSIGNED TX & RECOVER SENDER ADDRESS
# ==========================================
unsigned_tx = serializable_unsigned_transaction_from_dict(tx_dict)
derived_sender = None
encoded_tx = None
final_v = chain_v_base
EXPECTED_SENDER_PLACEHOLDER = "0xyour-mpc-key-address"
is_valid_expected = EXPECTED_SENDER and EXPECTED_SENDER != EXPECTED_SENDER_PLACEHOLDER
for parity in [0, 1]:
try_v = chain_v_base + parity
try:
tx_candidate = encode_transaction(unsigned_tx, vrs=(try_v, r, s))
addr = Account.recover_transaction("0x" + tx_candidate.hex())
if is_valid_expected and EXPECTED_SENDER.lower() == addr.lower():
derived_sender = addr
final_v = try_v
encoded_tx = tx_candidate
break
if not is_valid_expected and derived_sender is None:
derived_sender = addr
final_v = try_v
encoded_tx = tx_candidate
print(f"Warning: EXPECTED_SENDER not provided or is placeholder. Assuming first recovered address: {addr}")
except Exception:
continue
if encoded_tx is None:
if is_valid_expected:
raise ValueError(f"Could not recover address matching EXPECTED_SENDER: {EXPECTED_SENDER}")
else:
raise ValueError("Could not derive broadcastable transaction. Ensure signature matches payload.")
if derived_sender:
print(f"Derived Sender Address: {derived_sender}")
else:
print("Derived Sender Address: Could not recover (ensure signature matches current transaction payload)")
valid_tx_hex = "0x" + encoded_tx.hex()
print(f"Broadcastable Transaction Hex:\n{valid_tx_hex}\n")
# ==========================================
# 6. BROADCAST TO NETWORK
# ==========================================
payload = {
"jsonrpc": "2.0",
"method": "eth_sendRawTransaction",
"params": [valid_tx_hex],
"id": 1,
}
print(f"Broadcasting to {RPC_URL}...")
response = requests.post(RPC_URL, json=payload)
resp_json = response.json()
print("RPC Response:", resp_json)
if "result" in resp_json:
tx_hash = resp_json["result"]
print(f"View transaction on Sepolia Etherscan: https://sepolia.etherscan.io/tx/{tx_hash}")
EOF
python3 broadcast_eth.py
问题排查常见错误
invalid sender: recovery failed或invalid transaction v, r, s values:如果脚本中的交易参数与已签名的载荷不完全匹配,就会发生这种情况。确保value、nonce和to字段与第 5 步中处理的内容一致。insufficient funds for gas * price + value:您的交易在加密方面有效,并且已被节点接受,但您的 MPC 密钥的以太坊地址没有足够的测试网 ETH 来支付操作费用。您需要先为地址充值,然后才能广播。
比特币
SegWit 线路交易组装和广播
对于比特币原生隔离见证交易 (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