במדריך הזה נסביר על התהליכים שעוברים המשתמשים ב-Multi-party Compute API כדי לנהל מפתחות, משתמשים, צוותים וכללי מדיניות מבוזרים. הוא מיועד ללקוחות שמשתמשים בגרסת טרום-השקה (Preview) ולומדים את התהליכים המרכזיים שעוברים המשתמשים.
סקירה כללית
באמצעות MPC API אפשר לנהל את מחזור החיים של מכונות, צוותים, משתמשים, אוספי מפתחות, מפתחות מבוזרים וטקסי חתימה על סף, עם פיקוח קריפטוגרפי מובנה שנאכף בשלוש שכבות מדיניות:
- מדיניות ניהול (קוורום של תפקידים): אדמינים מוסיפים משתמשים וצוותים, ונדרש קוורום של אדמינים (
EnableUserRoleו-ApproveRoleChange) כדי להעניק או לבטל את התפקידים אדמין ויוצר מדיניות. הקצאת התפקיד חותם נכנסת לתוקף באופן מיידי על ידי אדמין יחיד, כי התפקיד חותם לבדו לא מעניק גישה למפתחות קיימים. חותם מקבל רק חלקי מפתחות כשהוא נכלל ב-KeyPolicy של KeyRing במהלך יצירת המפתח. - מדיניות מפתחות (
KeyRing.keyPolicy): אדמינים מגדירים את KeyPolicy של KeyRing כשהם יוצרים את KeyRing, ומציינים אילו חותמים (חותם, מקובצים לצוותים) מחזיקים במניות מפתח ואת אישורי החותמים של K-of-N (CreateTransactionו-RecordSignerDecision) שנדרשים כדי לאשר חתימה. - כללי מדיניות של טרנזקציות (
TransactionPolicyו-KeyList): קוורום של כותבי כללי מדיניות (POLICY_AUTHOR) מציע, מאשר או משבית כלליpermitו-forbidשל Cedar (TransactionPolicy) ורשימות היתרים של כתובותKeyList. כל בקשה שלCreateTransactionנבדקת מול כללי מדיניות פעילים של עסקאות (PERMITאוDENY) לפני החתימה.
דרישות מוקדמות
לפני שמתחילים, חשוב לוודא שיש לכם:
- Google Cloud פרויקט. כדי ליצור פרויקט, קראו את המאמר יצירת פרויקטים.
- אם לא משתמשים ב-Cloud Shell, צריך להתקין את ה-CLI של
gcloudולאמת אותו. ממשקי ה-API הנדרשים מופעלים, אם הם עדיין לא הופעלו. אתם צריכים שני ממשקי API: ממשק ה-API של MPC שמשמש במדריך הזה, ו-Cloud Key Management Service אם אתם משתמשים בו ליצירת מפתחות הרשאה ולחתימה.
gcloud services enable mpc.googleapis.com cloudkms.googleapis.comכדי לראות את ממשקי ה-API שמופעלים:
gcloud services list --enabledPython 3.9 ואילך וספריות עזר למדריכים מותקנות:
pip install embit eth-account eth-utils requests(אופציונלי) אם הארגון שלכם מבצע אימות באמצעות ספק זהויות חיצוני (כמו Okta או Microsoft Entra ID) בלי חשבונות Google, אתם יכולים לעיין במאמר הגדרת איחוד שירותי אימות הזהות של כוח עבודה (BYOID) כדי להגדיר את Application Default Credentials (ADC) עם הזהות המאוחדת שלכם.
הגדרה
מעדכנים את המשתנים הבאים כדי לאכלס את הפקודות במדריך הזה.
ההגדרה הנדרשת
חובה להתאים אישית את הפרמטרים האלה. המדריך לא יפעל עם ערכי ה-placeholder.
| פרמטר | ערך | תיאור |
|---|---|---|
| מזהה פרויקט | my-project |
מזהה פרויקט הצרכן של מכונת ה-MPC. |
| מזהה המכונה | my-mpc-instance |
המזהה של מכונת ה-MPC ([a-z]([a-z0-9-]{0,61}[a-z0-9])?). |
| Location | us-central1 |
מיקום הפריסה (לדוגמה, us-central1). |
| חשבון המשתמש של אדמין 1 | admin-1@example.com |
Google Cloud כתובת האימייל או חשבון ה-BYOID של המשתמש הראשון ב-ADMIN במופע. |
| חשבון משתמש של אדמין 2 | admin-2@example.com |
כתובת האימייל ב-Google Cloud או חשבון המשתמש ב-BYOID של המשתמש השני ב-ADMIN במכונה. |
| בעל החתימה הראשי הראשון | alice@example.com |
כתובת האימייל שלGoogle Cloud או חשבון BYOID של החותם הראשון (אליס). |
| בעל החתימה השני | bob@example.com |
כתובת האימייל או חשבון המשתמש ב-BYOID של החותם השני (בועז).Google Cloud |
| בעל חתימה מס' 3 | charlie@example.com |
Google Cloud כתובת האימייל או חשבון BYOID של החותם השלישי (צ'רלי). |
| Policy Author 1 Principal | dave@example.com |
כתובת האימייל או חשבון ה-BYOID של יוצר המדיניות הראשון (דוד) ב-Google Cloud . |
| Policy Author 2 Principal | eve@example.com |
כתובת האימייל או חשבון המשתמש ב-BYOID של כותב המדיניות השני (חוה).Google Cloud |
מאמתים כל חשבון פעם אחת בסשן הטרמינל, כדי ש-gcloud auth print-access-token "<principal>" יוכל לאחזר אסימונים לכל פרסונה בלי להחליף חשבונות באמצע המדריך (ADMIN_1_PRINCIPAL מאומת אחרון, כך שהוא נשאר החשבון הפעיל של gcloud CLI לפקודות של IAM ו-Cloud KMS):
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 במהלך המדריך הזה. ערכי ברירת המחדל תקינים ואפשר להשאיר אותם כדי לצמצם את החיכוך בהגדרה. שימו לב שמזהי המשאבים של Team, User, TransactionPolicy, KeyRing, Key, KeyList ו-Transaction נוצרים בשרת ונלכדים במשתני מעטפת (TEAM_NAME, SIGNER_1_NAME, POLICY_NAME, KEY_RING_NAME, KEY_NAME, KEY_LIST_NAME, TRANSACTION_NAME) כשכל משאב נוצר.
| פרמטר | ערך | תיאור |
|---|---|---|
| מזהה אוסף המפתחות ב-Cloud KMS | mpc-assent-keys |
אוסף מפתחות של Cloud Key Management Service למפתחות הרשאה חיצוניים שמאוחסנים ב-Cloud KMS. |
| מזהה אדמין 1 ב-Cloud KMS | admin-1 |
תווית מפתח ההרשאה של אדמין 1 ב-MPC. |
| מזהה האדמין השני של Cloud KMS | admin-2 |
תווית מפתח ההרשאה של MPC Admin 2. |
| מזהה החותם הראשון ב-Cloud KMS | signer-alice |
תווית מפתח ההרשאה של MPC Signer 1. |
| מזהה החותם השני ב-Cloud KMS | signer-bob |
תווית מפתח ההרשאה של MPC Signer 2. |
| מזהה Cloud KMS Signer 3 | signer-charlie |
תווית מפתח ההרשאה של MPC Signer 3. |
| מזהה 1 של מחבר המדיניות של Cloud KMS | author-dave |
תווית מפתח ההרשאה של מחבר המדיניות הראשון. |
| מזהה 2 של מחבר המדיניות של Cloud KMS | author-eve |
תווית מפתח ההרשאה של מחבר המדיניות השני. |
שלב 1: יוצרים מכונה של MPC
MPC Instance מייצג את סביבת ה-MPC הייעודית והמבודדת שלכם. כדי ליצור ולנהל מכונה, צריך את התפקיד אדמין של Multi-party Compute (roles/mpc.admin) ב-IAM בפרויקט. אתם יוצרים ובודקים את Instance כאדמינים באמצעות MPCInstanceService בנקודת הקצה של מישור הבקרה (https://mpc.googleapis.com), ושולחים את כל הבקשות הבאות למשאבים לכתובת ה-URL הייעודית של מישור הנתונים של המכונה (INSTANCE_URL).
א. יצירת המכונה
קודם כל, מוודאים שלחשבונות הראשיים של האדמינים (admin-1@example.com ו-admin-2@example.com) יש את ההרשאה roles/mpc.admin בפרויקט, וקוראים ל-instances.create (CreateInstance) בתור אדמין 1 כדי להקצות את מכונת ה-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 לפני שקוראים לכתובת ה-URL של מישור הנתונים שלה.
🕵️♂️ בדיקת מצב המכונה (סקר)
דוגמים את סטטוס המכונה עד שהערך של 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: רישום צוותים ומשתמשים
כדי שכל ישות תוכל להשתתף בחתימה על עסקאות (בתור SIGNER) או ביצירת מדיניות (בתור POLICY_AUTHOR), צריך לתת לה הרשאות IAM בפרויקט ולרשום אותה בתור User ברמת האפליקציה במכונה (במאמר אבטחה והרשאה מוסבר איך תפקידי IAM ותפקידי User פועלים יחד).
ההרשמה דורשת תיאום מחוץ לערוץ בין האדמין לבין המשתמש:
- הישות (החותם או יוצר המדיניות) יוצרת את זוג המפתחות שלה באופן מקומי ומעבירה את המפתח הציבורי שלה לאדמין באופן מאובטח.
- האדמין מקצה לחשבון המשתמש את התפקיד המתאים ב-IAM בפרויקט (
roles/mpc.editorלחותמים ולכותבי מדיניות,roles/mpc.adminלאדמינים) וקורא ל-MPC API כדי לרשום את ה-principalוה-authorizationKeyשל המשתמש.
א. יצירת צוות חותמים
כל User עם התפקיד SIGNER חייב להיות שייך ל-Team, וכל שאר ה-User עם תפקידים אחרים לא יכולים להיות שייכים ל-Team. בתור אדמין 1 (admin-1@example.com), יוצרים Team (CreateTeam) בשם "Treasury Signers" שיכיל את שלושת החותמים, ומתעדים את המשאב 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"
ב. הקצאת תפקידי IAM ורישום של חותמים וכותבי מדיניות
כל משתמש צריך תפקיד IAM ברמת הפרויקט (roles/mpc.editor לבעלי הרשאת חתימה ולכותבי מדיניות, ו-roles/mpc.admin לאדמינים) כדי להפעיל את MPC API, ומשאב User ברמת האפליקציה בתוך המכונה. כשמפעילים את users.create (CreateUser), השדה roles יכול להיות ריק או להכיל את הערך ["SIGNER"]:
- חותמים: קיבלו את ההרשאה
roles/mpc.editorב-IAM ונוצרו באמצעותroles: ["SIGNER"]ו-team: "$TEAM_NAME". התפקידSIGNERהופך מיד לתפקידACTIVEבלי צורך באישור של קוורום אדמינים, כי התפקידSIGNERלבדו לא מעניק גישה למפתחות קיימים. חותם מקבל חלקי מפתחות רק אם הוא נכלל ב-KeyPolicyשלKeyRingבמהלך יצירת המפתח. מנסחי מדיניות: מקבלים את ההרשאה
roles/mpc.editorב-IAM ונוצרים בלי תפקידים ראשוניים ובליteam, ואז מקבלים את התפקידPOLICY_AUTHORבשלב 3.ג באמצעות אישור קוורום של אדמינים (כי ל-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')
ג. הקצאת התפקיד "כותב מדיניות" (אישור קוורום של אדמינים)
כדי להפעיל או להשבית את התפקיד POLICY_AUTHOR או ADMIN ב-User, נדרש קוורום של אדמינים:
- אדמין 1 מפעיל את
users.enableRole(EnableUserRole), מה שמעביר את התפקיד ל-PENDING_ACTIVEומתעד את החתימה של אדמין 1 כאישור הראשון. - אדמין 2 קורא ל-
users.approveRoleChange(ApproveRoleChange) עם החתימה שלו, ומעביר את התפקיד ל-ACTIVE.
Cloud KMS
1. בקשה POLICY_AUTHOR בתור אדמין 1 (EnableUserRole)
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. בקשה POLICY_AUTHOR בתור אדמין 1 (EnableUserRole)
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).
א. יצירת מדיניות (PENDING_ACTIVE)
יוצרים כלל מדיניות ב-Cedar שמתאר אילו עסקאות מותרות. אפשר להגדיר הרבה כללי מדיניות לתרחישים שונים, אבל במדריך הזה נראה איך להגדיר מדיניות לעסקאות ב-Ethereum או בביטקוין. כברירת מחדל, הערכת המדיניות של העסקאות היא עצירה מיידית במקרה שגיאה (חסימה כברירת מחדל). אם אין מדיניות permit פעילה שתואמת לעסקה, ההחלטה של הערכת המדיניות היא DENY. לפרטים מלאים על מאפייני Cedar, קראו את המאמר כתיבת כללי מדיניות של טרנזקציות.
Ethereum, KMS
הכלל הזה מאפשר העברות קטנות יותר של אתריום (<= 2 ETH, מיוצג כ-2,000,000,000 ביחידות של 10-9 ETH / Gwei) לכתובת נמען ספציפית (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, מיוצג כ-2,000,000,000 ביחידות של 10-9 ETH / Gwei) לכתובת נמען ספציפית (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')
ביטקוין, 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')
ביטקוין, 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')
ב. אישור המדיניות (מעבר לסטטוס ACTIVE)
כדי לשנות את המדיניות נדרשות 2 חתימות מתוך 2, ולכן כותב מדיניות שני צריך להפעיל את transactionPolicies.approve (ApproveTransactionPolicy) עם TransactionPolicySigningPayload חתום.
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
ג. יצירה וניהול של KeyList (אופציונלי)
KeyList (KeyListService) היא רשימה עם שם של כתובות בלוקצ'יין (manualEntry, MPC key או keyRing) שכללי מדיניות יכולים להפנות אליהן כיעדים מהימנים. אדמינים (ADMIN) יכולים ליצור KeyList באמצעות keyLists.create ולעדכן את הרשומות שלו בכל שלב באמצעות 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), יוצרים KeyRing עם KeyPolicy מוטבע ומפיקים מפתח MPC מבוזר.
א. יצירת KeyRing עם KeyPolicy מוטבע
KeyRing הוא קיבוץ לוגי של מפתחות ה-MPC, והוא מגדיר KeyPolicy (keyRing.keyPolicy) מוטבע שלא ניתן לשינוי, שכל מפתח שנוצר ב-KeyRing פועל לפיו. KeyPolicy הוא עץ של כללי סף (PolicyNode) שהצאצא שלו subKeyShares יכול להיות teamNode (threshold של totalMembers ב-Team), userNode (User ספציפי של SIGNER) או policyNode בתצוגת עץ.
כאן אנחנו מגדירים כלל סף של 2 מתוך 3 על שלושת החותמים ב-${TEAM_NAME} (threshold: 2, totalMembers: 3, שמאוחסנים ב-GCP_TEE):
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"
ב. יצירת מפתח מבוזר (DKG)
מפעילים את keys.create (CreateKey) בתור אדמין 1 כדי להריץ יצירת מפתחות מבוזרת (DKG) בכל המשתתפים שנבחרו על ידי KeyPolicy של KeyRing.
הפונקציה CreateKey היא סינכרונית ומחזירה את המשאב ACTIVE Key, כולל publicKeyInfo.publicKey ו-publicKeyInfo.blockchainAddresses שנגזר מהשרת (EC_SIGN_SECP256K1 ל-Ethereum ול-Bitcoin, או EC_SIGN_ED25519 ל-Solana כשהיא נתמכת):
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')
ג. הפקת כתובת בלוקצ'יין ואיתור פלטי עסקאות שלא נוצלו (UTXO)
בסיום הפעולה של CreateKey, השדה publicKeyInfo.blockchainAddresses בתגובה כולל באופן אוטומטי כתובות נגזרות (secp256k1Addresses.ethereumAddress ו-secp256k1Addresses.bitcoinMainnetAddresses עבור EC_SIGN_SECP256K1, או ed25519Addresses.solanaAddress עבור EC_SIGN_ED25519). אפשר גם לאמת או לגזור באופן עצמאי כתובות Testnet וסקריפטים לנעילה מהמפתח הציבורי הלא דחוס בגודל 65 בייטים (0x04 || X || Y) באמצעות הסקריפטים הבאים, ולאתר כספים שלא נוצלו שנדרשים לעסקאות.
Ethereum
כתובות Ethereum מוחזרות ב-.publicKeyInfo.blockchainAddresses.secp256k1Addresses.ethereumAddress, ואפשר גם להפיק אותן מגיבוב Keccak-256 של נקודת המפתח הציבורי הלא דחוס של 64 בייטים (pub_bytes[1:]), תוך שימוש ב-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
מימון כתובת Ethereum
לפני השידור בשלב 7, ודאו שבכתובת הנגזרת של Ethereum יש מספיק ETH ברשת הבדיקה Sepolia כדי לכסות את ערך העסקה ואת עלויות הגז. כדי לבקש ETH בחינם ברשת הבדיקה, מזינים את הכתובת החדשה שנוצרה ב-Faucet for Ethereum Sepolia.
ביטקוין
כתובות Bitcoin Mainnet (p2wpkhAddress, p2shP2wpkhAddress ו-p2pkhAddress) מוחזרות ב-.publicKeyInfo.blockchainAddresses.secp256k1Addresses.bitcoinMainnetAddresses. מהמפתח הציבורי הלא דחוס של secp256k1 EC בגודל 65 בייטים (0x04 || X || Y), אתם יכולים גם להפיק את המפתח הציבורי הדחוס בגודל 33 בייטים, סקריפטים לנעילה וכתובות Testnet4 בכל שלושת הפורמטים:
- Native SegWit (P2WPKH / BIP 173): כתובת Bech32 (
bc1q...ב-Mainnet, tb1q...ב-Testnet4) עםscriptPubKey(0014<hash160>) של 22 בייטים. זה הפורמט המודרני המומלץ עם העמלות הנמוכות ביותר על עסקאות. - Nested SegWit (P2SH-P2WPKH / BIP 49): כתובת SegWit עטופה (
3...ב-Mainnet, 2...ב-Testnet4) עםscriptPubKey(a914<hash160>87) של 23 בייטים ו-redeemScript (0014<hash160>) של 22 בייטים, שתואמת למערכות מדור קודם. - מדור קודם (P2PKH / BIP 44): כתובת Base58 מקורית של Pay-to-PubKey-Hash (
1...ב-Mainnet, m/n...ב-Testnet4) עםscriptPubKey(76a914<hash160>88ac) של 25 בייטים.
סקריפט להפקת כתובת (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:
- מממנים את כתובת ה-Native SegWit שנגזרה (לדוגמה, באמצעות testnet faucet).
- שולחים שאילתה לגבי פלטי העסקאות שלא נוצלו (UTXO) של הכתובת שלכם באמצעות צומת הביטקוין (
bitcoin-cli listunspent 1 9999999 "[\"tb1q...\"]") או API של סייר בלוקים. - שימו לב ל-4 הפרמטרים הבאים לשלב 6:
-
txid: גיבוב (hash) של טרנזקציה בגודל 32 בייטים של הפלט שלא נוצל. -
vout: אינדקס הפלט שמתחיל באפס (לדוגמה,0או1). -
value: הסכום שלא נוצל בסאטושי (לדוגמה,200000סאטושי = 0.002 BTC). -
scriptPubKey: הסקריפט לנעילת הקסדצימלי (לדוגמה,0014...).
-
שלב 6: ביצוע פעולת MPC
הערכת המדיניות והתחלת החתימה מתרחשות יחד כש-SIGNER קורא ל-transactions.create (CreateTransaction). המכונה מפענחת את payload לפי schema, מעריכה את כל כללי המדיניות הפעילים של העסקאות, מתעדת את התוצאה ב-policyEvaluationRecord ומתעדת את ההחלטה של AUTHORIZE שהתחיל את החתימה.
א. יצירת עסקה (הערכת מדיניות והתחלת חתימה)
שולחים את העסקה בתור חותם 1 (alice@example.com) להערכת מדיניות ולחתימה:
Ethereum, KMS
🛑 מקרה א': דחייה (ערך גבוה מדי)
ניסיון ליצור עסקה של 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).
✅ מקרה ב': אישור (ערך מותר)
יוצרים עסקה של 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
🛑 מקרה א': דחייה (ערך גבוה מדי)
ניסיון ליצור עסקה של 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).
✅ מקרה ב': אישור (ערך מותר)
יוצרים עסקה של 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.
ביטקוין, KMS
במודל UTXO של ביטקוין, עסקאות מוציאות פלטים ספציפיים שלא נוצלו. ב-SegWit (BIP 143), כדי לחשב את גיבוב החתימה צריך לדעת את כמות הסאטושי ואת סקריפט הנעילה של הקלט שמוציאים.
כדי להעריך את המדיניות של ביטקוין, נדרש פורמט BIP 174 PSBT v0 (BITCOIN_TX_V2) עם PSBT_IN_WITNESS_UTXO מאוכלס לכל קלט.
סקריפט ליצירת PSBT (build_psbt.py)
מריצים את הבלוק הבא כדי להרכיב מטען ייעודי (payload) תקין של 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"
🛑 מקרה א': דחייה (חריגה ממגבלת העמלה)
ניסיון ליצור עסקה שבה עמלת הכרייה חורגת ממגבלת המדיניות של 10,000 סאטושי. בדוגמה הזו, הגדרת MINER_FEE_SATS = 15000 (וגם CHANGE_SATS = 135000) ב-build_psbt.py משתנה ל-max_fee_atomic = 150000 (חריגה ממגבלת המדיניות של 100000), ויוצרת את המטען הייעודי (payload) הבא שמפעיל החלטה של הערכת מדיניות 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).
✅ מקרה ב': אישור (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.
ביטקוין, OpenSSL
במודל UTXO של ביטקוין, עסקאות מוציאות פלטים ספציפיים שלא נוצלו. ב-SegWit (BIP 143), כדי לחשב את גיבוב החתימה צריך לדעת את כמות הסאטושי ואת סקריפט הנעילה של הקלט שמוציאים.
כדי להעריך את המדיניות של ביטקוין, נדרש פורמט BIP 174 PSBT v0 (BITCOIN_TX_V2) עם PSBT_IN_WITNESS_UTXO מאוכלס לכל קלט.
סקריפט ליצירת PSBT (build_psbt.py)
מריצים את הבלוק הבא כדי להרכיב מטען ייעודי (payload) תקין של 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"
🛑 מקרה א': דחייה (חריגה ממגבלת העמלה)
ניסיון ליצור עסקה שבה עמלת הכרייה חורגת ממגבלת המדיניות של 10,000 סאטושי. בדוגמה הזו, הגדרת MINER_FEE_SATS = 15000 (וגם CHANGE_SATS = 135000) ב-build_psbt.py משתנה ל-max_fee_atomic = 150000 (חריגה ממגבלת המדיניות של 100000), ויוצרת את המטען הייעודי (payload) הבא שמפעיל החלטה של הערכת מדיניות 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).
✅ מקרה ב': אישור (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
ב. תיעוד ההחלטה של החותם (אישור)
מכיוון ש-CreateTransaction בשלב 6.א כבר תיעד את ההחלטה של החותם הראשון AUTHORIZE (אחת מתוך שתיים שנדרשות לפי הסף של 2 מתוך 3 של KeyPolicy), רק שותף אחד נוסף לחתימה שמחזיק במפתח משותף צריך לאשר את העסקה באמצעות transactions.recordSignerDecision (RecordSignerDecision).
חותמים על TransactionSigningPayload, מציינים את transaction (${TRANSACTION_NAME}) וקוראים ל-recordSignerDecision בתור בעל החתימה השני (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
ג. אחזור החתימה הסופית
אחרי שמגיעים לסף ההרשאות, בודקים את מצב העסקה ומאחזרים את finalSignature (ואת recoveryId למפתחות EC_SIGN_SECP256K1):
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, מעתיקים את השדה finalSignature מה-JSON שהוחזר לשלב 7.
שלב 7: שידור העסקה לבלוקצ'יין
אחרי שהעסקה מגיעה למצב SIGNING_SUCCEEDED ואחרי שאחזרתם את finalSignature, השלב האחרון הוא ליצור את העסקה החתומה ולשדר אותה לרשת.
Ethereum
הפקה ושידור של מזהה השחזור
MPC API מחזיר חתימת ECDSA גולמית של 64 בייטים ב-finalSignature (שכוללת את הערכים של r ו-s בנפח 32 בייטים בפורמט low-S קנוני לפי EIP-2), יחד עם ביט הזוגיות לשחזור (0 או 1) ב-recoveryId.
סקריפט ה-Python הבא יוצר את העסקה החתומה באמצעות finalSignature, מאמת את כתובת השולח ששוחזרה מול כתובת ה-Ethereum של מפתח ה-MPC, ומשדר אותה לרשת.
תסריט לשידור:
מריצים את הבלוק הבא בטרמינל כדי לכתוב ולהריץ את 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: השגיאה הזו מתרחשת אם הפרמטרים של העסקה בסקריפט לא תואמים במדויק למטען הייעודי (payload) שנחתם. מוודאים שהשדותvalue,nonceו-toתואמים למה שעובד בשלב 6.-
insufficient funds for gas * price + value: העסקה שלכם תקפה מבחינה קריפטוגרפית והתקבלה על ידי הצומת, אבל לכתובת ה-Ethereum של מפתח ה-MPC שלכם אין מספיק ETH ברשת הבדיקה כדי לכסות את הפעולה. צריך להוסיף כסף לכתובת לפני השידור.
ביטקוין
הרכבה ושידור של עסקאות SegWit Wire
בעסקאות Bitcoin Native SegWit (BIP 141), צריך לעצב את חתימת ה-MPC הגולמית של 64 בייטים ((r, s)) ל-canonical low-S ASN.1 DER, להוסיף לה 0x01 (SIGHASH_ALL) ולהרכיב אותה עם המפתח הציבורי הדחוס של 33 בייטים למחסנית העדים של SegWit עם 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 בפרויקט ובמיקום שלכם.
א. הצגת רשימה של כל המכונות
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
ב. קבלת פרטים על מכונה ספציפית
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. צוותים ומשתמשים
בודקים את הצוותים והמשתמשים במכונה.
א. הצגת רשימה של כל הצוותים
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
ב. קבלת פרטים על צוות ספציפי
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
ג. הצגת רשימה של כל המשתמשים הרשומים
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
ד. קבלת פרטים על משתמש ספציפי
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. כללי מדיניות ורשימות מפתחות של עסקאות
בודקים את כללי המדיניות של העסקאות ואת רשימות המפתחות.
א. רשימה של כל כללי המדיניות בנושא עסקאות
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
ב. קבלת פרטים על מדיניות ספציפית בנושא עסקאות
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
ג. הצגת רשימה וטעינה של KeyLists
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. מפתחות ואוספי מפתחות
בדיקת אוספי מפתחות ומפתחות מבוזרים.
א. הצגת רשימה של כל אוספי המפתחות (KeyRings)
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
ב. הצגת רשימה של כל המפתחות ב-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/${KEY_RING_NAME}/keys" | jq
ג. קבלת פרטים של מפתח מבוזר ספציפי
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. עסקאות (פעולות חתימה)
לבדוק עסקאות ואת סטטוס החתימה.
א. הצגת רשימה של כל העסקאות שמשויכות למפתח (או לכל המפתחות)
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
ב. קבלת פרטים או סטטוס של עסקה ספציפית
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