Panduan Pengguna Multi-pihak Compute

Panduan ini membahas perjalanan pengguna Multi-party Compute API untuk mengelola kunci, pengguna, tim, dan kebijakan terdistribusi. Fitur ini ditujukan bagi pelanggan pratinjau yang mempelajari perjalanan pengguna utama.

Ringkasan

MPC API mengelola siklus proses instance, tim, pengguna, key ring, kunci terdistribusi, dan upacara penandatanganan nilai minimum, dengan tata kelola kriptografis bawaan yang diterapkan di tiga lapisan kebijakan:

  • Kebijakan Administrasi (Kuorum Peran): Admin melakukan aktivasi Pengguna dan Tim, dan kuorum Admin (EnableUserRole dan ApproveRoleChange) diperlukan untuk memberikan atau mencabut peran Admin dan Penulis Kebijakan. Pemberian peran Signer akan langsung berlaku dari satu Admin karena Signer saja tidak memberikan akses ke kunci yang ada; Signer hanya menerima bagian kunci jika disertakan dalam KeyPolicy KeyRing selama pembuatan kunci.
  • Kebijakan Kunci (KeyRing.keyPolicy): Admin menentukan KeyPolicy dari KeyRing saat membuat KeyRing, yang menentukan Penanda Tangan (Penanda Tangan, yang dikelompokkan ke dalam Tim) yang menyimpan pembagian kunci dan persetujuan penanda tangan K-of-N (CreateTransaction dan RecordSignerDecision) yang diperlukan untuk mengizinkan tanda tangan.
  • Kebijakan Transaksi (TransactionPolicy dan KeyList): Kuorum Penulis Kebijakan (POLICY_AUTHOR) mengusulkan, menyetujui, atau menonaktifkan aturan Cedar permit dan forbid (TransactionPolicy) serta daftar yang diizinkan alamat KeyList. Setiap permintaan CreateTransaction dievaluasi berdasarkan kebijakan transaksi aktif (PERMIT atau DENY) sebelum penandatanganan.

Prasyarat

Sebelum memulai, pastikan Anda memiliki:

  1. Project Google Cloud . Lihat Membuat project untuk membuatnya.
  2. Jika tidak menggunakan Cloud Shell, gcloud CLI harus diinstal dan diautentikasi.
  3. API yang diperlukan diaktifkan, jika belum dilakukan. Anda memerlukan dua API: MPC API yang digunakan dalam panduan ini, dan Cloud Key Management Service jika menggunakannya untuk pembuatan dan penandatanganan kunci otorisasi.

      gcloud services enable mpc.googleapis.com cloudkms.googleapis.com
    

    Untuk melihat API yang diaktifkan:

      gcloud services list --enabled
    
  4. Python 3.9+ dan library bantuan tutorial telah diinstal:

      pip install embit eth-account eth-utils requests
    
  5. (Opsional) Jika organisasi Anda melakukan autentikasi menggunakan Penyedia Identitas eksternal (seperti Okta atau Microsoft Entra ID) tanpa Akun Google, lihat Mengonfigurasi Workforce Identity Federation (BYOID) untuk mengonfigurasi Kredensial Default Aplikasi (ADC) dengan identitas gabungan Anda.

Penyiapan

Perbarui variabel berikut untuk mengisi perintah di seluruh panduan ini.

Penyiapan yang Diperlukan

Anda harus menyesuaikan parameter ini. Panduan ini tidak akan berfungsi dengan nilai placeholder.

Parameter Nilai Deskripsi
Project ID my-project ID project konsumen untuk instance MPC Anda.
Instance ID my-mpc-instance ID untuk instance MPC Anda ([a-z]([a-z0-9-]{0,61}[a-z0-9])?).
Location us-central1 Lokasi deployment (misalnya, us-central1).
Admin 1 Principal admin-1@example.com EmailGoogle Cloud atau akun utama BYOID dari pengguna ADMIN pertama dalam instance.
Admin 2 Principal admin-2@example.com EmailGoogle Cloud atau akun utama BYOID dari pengguna ADMIN kedua dalam instance.
Akun Utama Penanda Tangan 1 alice@example.com EmailGoogle Cloud atau akun utama BYOID Penanda Tangan 1 (Alice).
Akun Utama Penanda Tangan 2 bob@example.com EmailGoogle Cloud atau akun utama BYOID Penanda Tangan 2 (Budi).
Akun Utama Penanda Tangan 3 charlie@example.com EmailGoogle Cloud atau akun utama BYOID Penanda Tangan 3 (Charlie).
Policy Author 1 Principal dave@example.com EmailGoogle Cloud atau akun utama BYOID dari Penulis Kebijakan 1 (Dave).
Policy Author 2 Principal eve@example.com Google Cloud email atau akun utama BYOID dari Penulis Kebijakan 2 (Eve).

Autentikasi setiap akun sekali dalam sesi terminal Anda agar gcloud auth print-access-token "<principal>" dapat mengambil token untuk persona apa pun tanpa beralih akun di tengah tutorial (ADMIN_1_PRINCIPAL diautentikasi terakhir sehingga tetap menjadi akun gcloud CLI yang aktif untuk perintah IAM dan 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

Label Resource Tutorial

Ini adalah ID lokal untuk kunci otorisasi yang dibuat menggunakan Cloud KMS selama tutorial ini. Nilai defaultnya valid dan dapat dipertahankan untuk meminimalkan hambatan penyiapan. Perhatikan bahwa ID resource untuk resource Team, User, TransactionPolicy, KeyRing, Key, KeyList, dan Transaction dibuat oleh server dan dicatat ke dalam variabel shell (TEAM_NAME, SIGNER_1_NAME, POLICY_NAME, KEY_RING_NAME, KEY_NAME, KEY_LIST_NAME, TRANSACTION_NAME) saat setiap resource dibuat.

Parameter Nilai Deskripsi
ID KeyRing Cloud KMS mpc-assent-keys KeyRing Cloud Key Management Service untuk kunci otorisasi eksternal yang disimpan di Cloud KMS.
ID Cloud KMS Admin 1 admin-1 Label kunci otorisasi MPC Admin 1.
ID Cloud KMS Admin 2 admin-2 Label kunci otorisasi Admin MPC 2.
ID Penanda Tangan 1 Cloud KMS signer-alice Label kunci otorisasi MPC Signer 1.
ID Penanda Tangan Cloud KMS 2 signer-bob Label kunci otorisasi Penanda Tangan MPC 2.
ID Penanda Tangan 3 Cloud KMS signer-charlie Label kunci otorisasi MPC Signer 3.
ID Penulis Kebijakan Cloud KMS 1 author-dave Label kunci otorisasi Policy Author 1.
ID Cloud KMS Policy Author 2 author-eve Label kunci otorisasi Policy Author 2.

Langkah 1: Buat Instance MPC

MPC Instance mewakili lingkungan MPC khusus yang terisolasi. Membuat dan mengelola instance memerlukan peran IAM Multi-party Compute Admin (roles/mpc.admin) di project Anda. Anda membuat dan memeriksa Instance sebagai Admin menggunakan MPCInstanceService di endpoint bidang kontrol (https://mpc.googleapis.com), dan mengirim semua permintaan resource berikutnya ke URL bidang data khusus instance (INSTANCE_URL).

A. Membuat Instance

Pertama, pastikan akun utama Admin Anda (admin-1@example.com dan admin-2@example.com) memiliki roles/mpc.admin di project, dan panggil instances.create (CreateInstance) sebagai Admin 1 untuk menyediakan instance MPC Anda:

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

Tindakan ini akan menampilkan operasi yang berjalan lama (LRO). Anda harus menunggu Instance state menjadi ACTIVE sebelum memanggil URL bidang datanya.

🕵️‍♂️ Memeriksa Status Instance (Polling)

Lakukan polling status instance hingga state menjadi ACTIVE, dan simpan kolom url-nya ke 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"

Langkah 2: Buat Kunci Otorisasi

Untuk menggunakan MPC API, Anda harus membuat keypair untuk peran berbeda berikut. Perhatikan bahwa ini adalah kunci otorisasi kriptografis eksternal (User.authorizationKey), yang berarti Anda membuat dan menyimpannya di luar instance MPC, sehingga berbeda dari pembagian kunci MPC yang dibuat selama pembuatan kunci:

  • Admin: Memberikan otorisasi perubahan pada peran pengguna ADMIN dan POLICY_AUTHOR, serta membuat Key.
  • Policy Authors: Memberikan otorisasi kebijakan tata kelola (TransactionPolicy).
  • Penanda tangan: Memberikan otorisasi (AUTHORIZE atau DENY) pada transaksi.

Buat kunci otorisasi kriptografis secp256k1 untuk Penanda Tangan, Penulis Kebijakan, dan Admin Anda. Perintah ini bersifat idempoten.

Cloud KMS

Sebaiknya kelola kunci otorisasi di Cloud Key Management Service. Pastikan identitas Anda memiliki izin untuk membuat key ring, membuat kunci, menandatangani, dan melihat pemverifikasi kunci publik (seperti roles/cloudkms.admin dan roles/cloudkms.signerVerifier). Pastikan Anda telah membuat KeyRing, lalu buat kunci secp256k1 untuk setiap Penanda Tangan, Penulis Kebijakan, dan Admin.

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

Anda dapat menggunakan OpenSSL standar untuk membuat kunci secp256k1 bagi setiap Penanda Tangan, Penulis Kebijakan, dan Admin.

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

Langkah 3: Daftarkan Tim dan Pengguna

Sebelum entitas apa pun dapat berpartisipasi dalam penandatanganan transaksi (sebagai SIGNER) atau penulisan kebijakan (sebagai POLICY_AUTHOR), entitas tersebut harus diberi izin IAM pada project dan didaftarkan sebagai User level aplikasi dalam instance (lihat Keamanan dan otorisasi untuk mengetahui cara kerja peran IAM dan peran User).

Pendaftaran memerlukan koordinasi out-of-band antara Administrator dan Pengguna:

  1. Entitas (Penanda Tangan atau Penulis Kebijakan) membuat pasangan kuncinya secara lokal dan mentransfer Kunci Publik-nya kepada Administrator dengan aman.
  2. Administrator memberikan peran IAM yang sesuai kepada akun utama Pengguna di project (roles/mpc.editor untuk Penanda Tangan dan Penulis Kebijakan; roles/mpc.admin untuk Admin) dan memanggil MPC API untuk mendaftarkan principal dan authorizationKey Pengguna.

A. Membuat Tim Penanda Tangan

Setiap User dengan peran SIGNER harus termasuk dalam Team, dan semua User lain dengan peran lain tidak boleh termasuk dalam Team. Sebagai Admin 1 (admin-1@example.com), buat Team (CreateTeam) "Treasury Signers" untuk menyimpan tiga penanda tangan Anda dan mengambil name resource yang dihasilkan server:

curl

TEAM_RESPONSE=$(curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d '{
    "displayName": "Treasury Signers",
  }' \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/teams?requestId=11111111-1111-4111-8111-111111111101")

echo "$TEAM_RESPONSE" | jq
export TEAM_NAME=$(echo "$TEAM_RESPONSE" | jq -r '.name')
echo "Created Team: $TEAM_NAME"

B. Memberikan Peran IAM serta Mendaftarkan Penanda Tangan dan Penulis Kebijakan

Setiap pengguna memerlukan peran IAM level project (roles/mpc.editor untuk Penanda Tangan dan Penulis Kebijakan, serta roles/mpc.admin untuk Admin) untuk memanggil MPC API dan resource User level aplikasi di dalam instance. Saat memanggil users.create (CreateUser), kolom roles hanya boleh kosong atau ["SIGNER"]:

  • Penanda tangan: Diberi roles/mpc.editor di IAM dan dibuat dengan roles: ["SIGNER"] dan team: "$TEAM_NAME". Peran SIGNER segera menjadi ACTIVE tanpa memerlukan persetujuan kuorum Admin, karena memiliki peran SIGNER saja tidak memberikan akses ke kunci yang ada; Penanda Tangan hanya menerima bagian kunci jika disertakan dalam KeyPolicy KeyRing selama pembuatan kunci.
  • Policy Authors: Diberi roles/mpc.editor di IAM dan dibuat tanpa peran awal dan tanpa team, lalu diberi peran POLICY_AUTHOR di Langkah 3.C menggunakan persetujuan kuorum Admin (karena POLICY_AUTHOR dan ADMIN memiliki otoritas tata kelola di seluruh instance).

curl

provision_user() {
  local id=$1
  local display_name=$2
  local principal=$3
  local request_id=$4
  local team=$5
  local pubkey_b64
  local signer_fields=""

  # 1. Grant project-level IAM permission (roles/mpc.editor) to the User's principal
  grant_iam_role "$principal" "roles/mpc.editor" >&2

  # 2. Read the public key from the file generated in Step 2 (Base64-encoded PEM/DER bytes)
  pubkey_b64=$(openssl base64 -A -in "${id}_pub.pem")

  if [[ -n "$team" ]]; then
    signer_fields=", \"roles\": [\"SIGNER\"], \"team\": \"${team}\""
  fi

  # 3. Register the application-level User in the MPC instance as Admin 1
  curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d "{
      \"displayName\": \"${display_name}\",
      \"principal\": \"${principal}\",
      \"authorizationKey\": {
        \"publicKey\": \"${pubkey_b64}\",
        \"algorithm\": \"PUBLIC_KEY_SIGNATURE_ALGORITHM_ECDSA_SECP256K1\"
      }${signer_fields}
    }" \
    "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/users?requestId=${request_id}"
}

echo "Registering Signers..."
export SIGNER_1_NAME=$(provision_user "signer-alice" "Signer Alice" "alice@example.com" "11111111-1111-4111-8111-111111111001" "${TEAM_NAME}" | tee /dev/stderr | jq -r '.name')
export SIGNER_2_NAME=$(provision_user "signer-bob" "Signer Bob" "bob@example.com" "11111111-1111-4111-8111-111111111002" "${TEAM_NAME}" | tee /dev/stderr | jq -r '.name')
export SIGNER_3_NAME=$(provision_user "signer-charlie" "Signer Charlie" "charlie@example.com" "11111111-1111-4111-8111-111111111003" "${TEAM_NAME}" | tee /dev/stderr | jq -r '.name')

echo "Registering Policy Authors..."
export POLICY_AUTHOR_1_NAME=$(provision_user "author-dave" "Author Dave" "dave@example.com" "11111111-1111-4111-8111-111111111004" | tee /dev/stderr | jq -r '.name')
export POLICY_AUTHOR_2_NAME=$(provision_user "author-eve" "Author Eve" "eve@example.com" "11111111-1111-4111-8111-111111111005" | tee /dev/stderr | jq -r '.name')

C. Memberikan Peran Penulis Kebijakan (Persetujuan Kuorum Admin)

Mengaktifkan atau menonaktifkan peran POLICY_AUTHOR atau ADMIN di User memerlukan kuorum Admin:

  1. Admin 1 memanggil users.enableRole (EnableUserRole), yang memindahkan peran ke PENDING_ACTIVE dan mencatat tanda tangan Admin 1 sebagai persetujuan pertama.
  2. Admin 2 memanggil users.approveRoleChange (ApproveRoleChange) dengan tanda tangannya, sehingga mentransisikan peran ke ACTIVE.

Cloud KMS

1. Minta POLICY_AUTHOR sebagai Admin 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. Setujui POLICY_AUTHOR sebagai Admin 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. Minta POLICY_AUTHOR sebagai Admin 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. Setujui POLICY_AUTHOR sebagai Admin 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"

Langkah 4: Konfigurasi Kebijakan Transaksi

Konfigurasikan kebijakan transaksi dengan membuat dan menyetujui TransactionPolicy (dan secara opsional KeyList).

A. Membuat Kebijakan (PENDING_ACTIVE)

Buat aturan kebijakan Cedar yang menjelaskan transaksi apa yang diizinkan. Meskipun Anda dapat mengonfigurasi banyak kebijakan untuk mengatur berbagai skenario, panduan ini menunjukkan cara menyiapkan kebijakan untuk transaksi Ethereum atau Bitcoin. Secara default, evaluasi kebijakan transaksi adalah Fail-Fast (Blokir secara Default); jika tidak ada kebijakan permit aktif yang cocok dengan transaksi, keputusan evaluasi kebijakan adalah DENY. Untuk mengetahui detail lengkap tentang atribut Cedar, lihat Menulis aturan kebijakan transaksi.

Ethereum, KMS

Aturan ini mengizinkan transfer Ethereum yang lebih kecil (<= 2 ETH, yang direpresentasikan sebagai 2,000,000,000 dalam satuan 10-9 ETH / Gwei) ke alamat penerima tertentu (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

Aturan ini mengizinkan transfer Ethereum yang lebih kecil (<= 2 ETH, yang direpresentasikan sebagai 2,000,000,000 dalam satuan 10-9 ETH / Gwei) ke alamat penerima tertentu (0x1111111111111111111111111111111111111111).

permit (principal, action, resource)
when {
  context.schema == "ETHEREUM_TX_V2" &&
  context.secondary_key == "0x1111111111111111111111111111111111111111" &&
  context has amount_token_scaled &&
  context.amount_token_scaled <= 2000000000
};
sign_policy() {
  local author_id=$1
  local resource_type=$2
  local resource_name=$3
  local action=$4
  local cedar_doc=$5
  python3 -c 'import sys; rt, rn, act, doc = sys.argv[1:]; v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("policy_payload.bin", "wb").write(f(1 if rt == "instance" else 2, rn.encode()) + bytes([24, 1 if act == "ACTIVATE" else 2]) + f(4, f(1, doc.encode())))' "${resource_type}" "${resource_name}" "${action}" "${cedar_doc}"
  openssl dgst -sha256 -sign "${author_id}_priv.pem" -out policy_sig.bin policy_payload.bin
  openssl base64 -A -in policy_sig.bin
}

export CEDAR_RULE='permit (principal, action, resource) when { context.schema == "ETHEREUM_TX_V2" && context.secondary_key == "0x1111111111111111111111111111111111111111" && context has amount_token_scaled && context.amount_token_scaled <= 2000000000 };'

# Sign as Policy Author 1
QUORUM_SIGNATURE=$(sign_policy "author-dave" "instance" "${INSTANCE_NAME}" "ACTIVATE" "${CEDAR_RULE}")
POLICY_PAYLOAD_B64=$(openssl base64 -A -in policy_payload.bin)

POLICY_RESPONSE=$(jq -n \
  --arg rule "$CEDAR_RULE" \
  --arg sig "$QUORUM_SIGNATURE" \
  --arg payload "$POLICY_PAYLOAD_B64" \
  '{
    transactionPolicy: {
      displayName: "Ethereum Transfer Permit Policy",
      rule: {cedarDocument: $rule}
    },
    authorizationInfo: {
      signature: $sig,
      signedPayload: $payload
    },
    requestId: "11111111-1111-4111-8111-111111111201"
  }' \
| curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "dave@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d @- \
    "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/transactionPolicies")

echo "$POLICY_RESPONSE" | jq
export POLICY_NAME=$(echo "$POLICY_RESPONSE" | jq -r '.name')

Bitcoin, KMS

Aturan ini mengizinkan transfer Bitcoin menggunakan skema BITCOIN_TX_V2 (BIP 174 PSBT v0) dengan jumlah transfer keluar bersih tidak melebihi 250.000 satoshi (context.amount_token_scaled <= 2500000, atau 0,0025 BTC yang dinormalisasi menjadi 10-9 unit desimal tetap) dan biaya penambang tidak melebihi 10.000 satoshi (context.max_fee_atomic <= 100000).

permit (principal, action, resource)
when {
  context.schema == "BITCOIN_TX_V2" &&
  context has amount_token_scaled &&
  context.amount_token_scaled <= 2500000 &&
  context.max_fee_atomic <= 100000
};
sign_policy() {
  local author_id=$1
  local resource_type=$2
  local resource_name=$3
  local action=$4
  local cedar_doc=$5
  python3 -c 'import sys; rt, rn, act, doc = sys.argv[1:]; v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("policy_payload.bin", "wb").write(f(1 if rt == "instance" else 2, rn.encode()) + bytes([24, 1 if act == "ACTIVATE" else 2]) + f(4, f(1, doc.encode())))' "${resource_type}" "${resource_name}" "${action}" "${cedar_doc}"
  gcloud kms asymmetric-sign \
    --version=1 \
    --key="${author_id}-key" \
    --keyring="mpc-assent-keys" \
    --location="us-central1" \
    --digest-algorithm=sha256 \
    --input-file=policy_payload.bin \
    --signature-file=policy_sig.bin > /dev/null
  openssl base64 -A -in policy_sig.bin
}

export CEDAR_RULE='permit (principal, action, resource) when { context.schema == "BITCOIN_TX_V2" && context has amount_token_scaled && context.amount_token_scaled <= 2500000 && context.max_fee_atomic <= 100000 };'

# Sign as Policy Author 1
QUORUM_SIGNATURE=$(sign_policy "author-dave" "instance" "${INSTANCE_NAME}" "ACTIVATE" "${CEDAR_RULE}")
POLICY_PAYLOAD_B64=$(openssl base64 -A -in policy_payload.bin)

POLICY_RESPONSE=$(jq -n \
  --arg rule "$CEDAR_RULE" \
  --arg sig "$QUORUM_SIGNATURE" \
  --arg payload "$POLICY_PAYLOAD_B64" \
  '{
    transactionPolicy: {
      displayName: "Bitcoin PSBT Permit Policy",
      rule: {cedarDocument: $rule}
    },
    authorizationInfo: {
      signature: $sig,
      signedPayload: $payload
    },
    requestId: "11111111-1111-4111-8111-111111111202"
  }' \
| curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "dave@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d @- \
    "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/transactionPolicies")

echo "$POLICY_RESPONSE" | jq
export POLICY_NAME=$(echo "$POLICY_RESPONSE" | jq -r '.name')

Bitcoin, OpenSSL

Aturan ini mengizinkan transfer Bitcoin menggunakan skema BITCOIN_TX_V2 (BIP 174 PSBT v0) dengan jumlah transfer keluar bersih tidak melebihi 250.000 satoshi (context.amount_token_scaled <= 2500000, atau 0,0025 BTC yang dinormalisasi menjadi 10-9 unit desimal tetap) dan biaya penambang tidak melebihi 10.000 satoshi (context.max_fee_atomic <= 100000).

permit (principal, action, resource)
when {
  context.schema == "BITCOIN_TX_V2" &&
  context has amount_token_scaled &&
  context.amount_token_scaled <= 2500000 &&
  context.max_fee_atomic <= 100000
};
sign_policy() {
  local author_id=$1
  local resource_type=$2
  local resource_name=$3
  local action=$4
  local cedar_doc=$5
  python3 -c 'import sys; rt, rn, act, doc = sys.argv[1:]; v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("policy_payload.bin", "wb").write(f(1 if rt == "instance" else 2, rn.encode()) + bytes([24, 1 if act == "ACTIVATE" else 2]) + f(4, f(1, doc.encode())))' "${resource_type}" "${resource_name}" "${action}" "${cedar_doc}"
  openssl dgst -sha256 -sign "${author_id}_priv.pem" -out policy_sig.bin policy_payload.bin
  openssl base64 -A -in policy_sig.bin
}

export CEDAR_RULE='permit (principal, action, resource) when { context.schema == "BITCOIN_TX_V2" && context has amount_token_scaled && context.amount_token_scaled <= 2500000 && context.max_fee_atomic <= 100000 };'

# Sign as Policy Author 1
QUORUM_SIGNATURE=$(sign_policy "author-dave" "instance" "${INSTANCE_NAME}" "ACTIVATE" "${CEDAR_RULE}")
POLICY_PAYLOAD_B64=$(openssl base64 -A -in policy_payload.bin)

POLICY_RESPONSE=$(jq -n \
  --arg rule "$CEDAR_RULE" \
  --arg sig "$QUORUM_SIGNATURE" \
  --arg payload "$POLICY_PAYLOAD_B64" \
  '{
    transactionPolicy: {
      displayName: "Bitcoin PSBT Permit Policy",
      rule: {cedarDocument: $rule}
    },
    authorizationInfo: {
      signature: $sig,
      signedPayload: $payload
    },
    requestId: "11111111-1111-4111-8111-111111111202"
  }' \
| curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "dave@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d @- \
    "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/transactionPolicies")

echo "$POLICY_RESPONSE" | jq
export POLICY_NAME=$(echo "$POLICY_RESPONSE" | jq -r '.name')

B. Menyetujui Kebijakan (Transisi ke ACTIVE)

Perubahan kebijakan memerlukan 2 dari 2 tanda tangan, sehingga Penulis Kebijakan kedua harus memanggil transactionPolicies.approve (ApproveTransactionPolicy) dengan TransactionPolicySigningPayload yang ditandatangani.

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

Bitcoin

# 1. Check the policy state
POLICY_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth print-access-token "eve@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${POLICY_NAME}")

POLICY_STATE=$(echo "$POLICY_INFO" | jq -r '.state // "UNKNOWN"')

if [ "$POLICY_STATE" == "PENDING_ACTIVE" ]; then
  # 2. Sign as Policy Author 2 naming transaction_policy ($POLICY_NAME)
  QUORUM_SIGNATURE_2=$(sign_policy "author-eve" "transaction_policy" "${POLICY_NAME}" "ACTIVATE" "${CEDAR_RULE}")
  POLICY_PAYLOAD_2_B64=$(openssl base64 -A -in policy_payload.bin)

  # 3. Call ApproveTransactionPolicy as Policy Author 2
  curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "eve@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d "{
      \"authorizationInfo\": {
        \"signature\": \"${QUORUM_SIGNATURE_2}\",
        \"signedPayload\": \"${POLICY_PAYLOAD_2_B64}\"
      }
    }" \
    "${INSTANCE_URL}/v1beta/${POLICY_NAME}:approve" | jq
elif [ "$POLICY_STATE" == "ACTIVE" ]; then
  echo "Policy '${POLICY_NAME}' is already ACTIVE. Skipping approval."
else
  echo "Policy '${POLICY_NAME}' is in an unexpected state: $POLICY_STATE"
fi

C. Membuat dan Mengelola KeyList (Opsional)

KeyList (KeyListService) adalah daftar bernama alamat blockchain (manualEntry, MPC key, atau keyRing) yang dapat dirujuk oleh kebijakan sebagai tujuan tepercaya. Admin (ADMIN) dapat membuat KeyList dengan keyLists.create dan memperbarui entrinya kapan saja dengan keyLists.addEntries dan 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')

Langkah 5: Buat Kunci

Sebagai Admin 1 (admin-1@example.com), buat KeyRing dengan KeyPolicy inline dan buat kunci MPC terdistribusi.

A. Membuat KeyRing dengan KeyPolicy inline

KeyRing adalah grup logis untuk kunci MPC Anda dan menentukan KeyPolicy (keyRing.keyPolicy) inline yang tidak dapat diubah yang diikuti oleh setiap kunci yang dibuat di KeyRing. KeyPolicy adalah hierarki aturan nilai minimum (PolicyNode) yang subKeyShares turunannya dapat berupa teamNode (threshold dari totalMembers dalam Team), userNode (SIGNER User tertentu), atau policyNode bertingkat.

Di sini, kita mengonfigurasi aturan nilai minimum 2-of-3 di tiga penanda tangan di ${TEAM_NAME} (threshold: 2, totalMembers: 3, yang disimpan di 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"

B. Membuat Kunci Terdistribusi (DKG)

Panggil keys.create (CreateKey) sebagai Admin 1 untuk menjalankan Distributed Key Generation (DKG) di seluruh peserta yang dipilih oleh KeyPolicy KeyRing Anda.

CreateKey bersifat sinkron dan menampilkan resource ACTIVE Key, termasuk publicKeyInfo.publicKey dan publicKeyInfo.blockchainAddresses yang berasal dari server (EC_SIGN_SECP256K1 untuk Ethereum dan Bitcoin, atau EC_SIGN_ED25519 untuk Solana setelah didukung):

curl

KEY_RESPONSE=$(curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d '{
    "algorithm": "EC_SIGN_SECP256K1"
  }' \
  "${INSTANCE_URL}/v1beta/${KEY_RING_NAME}/keys?requestId=11111111-1111-4111-8111-111111111501")

echo "$KEY_RESPONSE" | jq
export KEY_NAME=$(echo "$KEY_RESPONSE" | jq -r '.name')
export PUBLIC_KEY_B64=$(echo "$KEY_RESPONSE" | jq -r '.publicKeyInfo.publicKey')

C. Mendapatkan Alamat Blockchain & Menemukan UTXO

Setelah CreateKey selesai, publicKeyInfo.blockchainAddresses dalam respons akan otomatis menyertakan alamat turunan (secp256k1Addresses.ethereumAddress dan secp256k1Addresses.bitcoinMainnetAddresses untuk EC_SIGN_SECP256K1, atau ed25519Addresses.solanaAddress untuk EC_SIGN_ED25519). Anda juga dapat memverifikasi atau memperoleh alamat Testnet dan skrip penguncian secara independen dari kunci publik yang tidak dikompresi sebesar 65 byte (0x04 || X || Y) menggunakan skrip berikut, dan menemukan dana yang belum dibelanjakan yang diperlukan untuk transaksi.

Ethereum

Alamat Ethereum ditampilkan dalam .publicKeyInfo.blockchainAddresses.secp256k1Addresses.ethereumAddress dan juga dapat diperoleh dari hash Keccak-256 dari titik kunci publik yang tidak dikompresi 64 byte (pub_bytes[1:]), dengan mengambil 20 byte terakhir.

Skrip Derivasi Alamat (derive_address.py)

Jalankan blok berikut untuk membuat dan menjalankan 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

Mendanai Alamat Ethereum

Sebelum melakukan siaran di Langkah 7, pastikan alamat Ethereum turunan Anda memiliki ETH testnet Sepolia yang cukup untuk menutupi nilai transaksi dan biaya gas. Anda dapat meminta ETH testnet gratis dengan memasukkan alamat yang baru Anda peroleh ke Faucet untuk Ethereum Sepolia.

Bitcoin

Alamat Mainnet Bitcoin (p2wpkhAddress, p2shP2wpkhAddress, dan p2pkhAddress) ditampilkan dalam .publicKeyInfo.blockchainAddresses.secp256k1Addresses.bitcoinMainnetAddresses. Dari kunci publik EC secp256k1 65 byte yang tidak dikompresi (0x04 || X || Y), Anda juga dapat memperoleh kunci publik terkompresi 33 byte, skrip penguncian, dan alamat Testnet4 di ketiga format:

  1. Native SegWit (P2WPKH / BIP 173): Alamat Bech32 (bc1q... di Mainnet, tb1q... di Testnet4) dengan scriptPubKey (0014<hash160>) witness 22 byte. Ini adalah format modern yang direkomendasikan dengan biaya transaksi terendah.
  2. Nested SegWit (P2SH-P2WPKH / BIP 49): Alamat SegWit yang digabungkan (3... di Mainnet, 2... di Testnet4) dengan scriptPubKey (a914<hash160>87) 23 byte dan redeemScript (0014<hash160>) 22 byte, kompatibel dengan sistem lama.
  3. Lama (P2PKH / BIP 44): Alamat Base58 Pay-to-PubKey-Hash asli (1... di Mainnet, m/n... di Testnet4) dengan scriptPubKey (76a914<hash160>88ac) 25 byte.

Skrip Derivasi Alamat (derive_btc_address.py)

Jalankan blok berikut untuk membuat dan menjalankan 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

Menemukan Output yang Belum Dibelanjakan (UTXO)

Tidak seperti blockchain berbasis akun, transaksi Bitcoin menggunakan output transaksi yang belum digunakan (UTXO) tertentu. Sebelum membuat transaksi di Langkah 6:

  1. Danai alamat Native SegWit turunan Anda (misalnya, menggunakan faucet testnet).
  2. Buat kueri UTXO yang belum dibelanjakan dari alamat Anda menggunakan node Bitcoin (bitcoin-cli listunspent 1 9999999 "[\"tb1q...\"]") atau API penjelajah blok.
  3. Perhatikan 4 parameter berikut untuk Langkah 6:
    • txid: Hash transaksi 32 byte dari output yang belum digunakan.
    • vout: Indeks output yang diindeks nol (misalnya, 0 atau 1).
    • value: Jumlah yang belum dibelanjakan dalam satoshi (misalnya, 200000 satoshi = 0,002 BTC).
    • scriptPubKey: Hex skrip penguncian (misalnya, 0014...).

Langkah 6: Menjalankan Operasi MPC

Evaluasi kebijakan dan inisiasi penandatanganan terjadi bersamaan saat SIGNER memanggil transactions.create (CreateTransaction). Instance mendekode payload sesuai dengan schema, mengevaluasi semua kebijakan transaksi aktif, mencatat hasilnya di policyEvaluationRecord, dan mencatat keputusan AUTHORIZE penanda tangan yang memulai.

A. Membuat Transaksi (Mengevaluasi Kebijakan & Memulai Penandatanganan)

Kirim transaksi sebagai Penanda Tangan 1 (alice@example.com) untuk evaluasi dan penandatanganan kebijakan:

Ethereum, KMS

🛑 Kasus A: Penolakan (Nilai Terlalu Tinggi)

Mencoba membuat transaksi sebesar 3 ETH (melebihi batas kebijakan 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

Hasil Evaluasi Kebijakan yang Diharapkan: decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED).

✅ Kasus B: Persetujuan (Nilai yang Diizinkan)

Buat transaksi sebesar 1 Gwei (dalam batas 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')

Respons yang Seharusnya: state: PENDING_AUTHORIZATION dan policyEvaluationRecord.decision: PERMIT.

Ethereum, OpenSSL

🛑 Kasus A: Penolakan (Nilai Terlalu Tinggi)

Mencoba membuat transaksi sebesar 3 ETH (melebihi batas kebijakan 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

Hasil Evaluasi Kebijakan yang Diharapkan: decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED).

✅ Kasus B: Persetujuan (Nilai yang Diizinkan)

Buat transaksi sebesar 1 Gwei (dalam batas 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')

Respons yang Seharusnya: state: PENDING_AUTHORIZATION dan policyEvaluationRecord.decision: PERMIT.

Bitcoin, KMS

Dalam model UTXO Bitcoin, transaksi membelanjakan output tertentu yang belum dibelanjakan. Di SegWit (BIP 143), penghitungan hash tanda tangan memerlukan pengetahuan tentang jumlah satoshi dan skrip penguncian input yang dibelanjakan.

Evaluasi kebijakan Bitcoin memerlukan format BIP 174 PSBT v0 (BITCOIN_TX_V2) dengan PSBT_IN_WITNESS_UTXO yang diisi untuk setiap input.

Skrip Konstruksi PSBT (build_psbt.py)

Jalankan blok berikut untuk menyusun payload PSBT v0 BIP 174 yang valid:

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"

🛑 Kasus A: Penolakan (Batas Biaya Terlampaui)

Mencoba membuat transaksi dengan biaya penambang yang melebihi batas kebijakan 10.000 satoshi. Dalam contoh ini, menetapkan MINER_FEE_SATS = 15000 (dan CHANGE_SATS = 135000) di build_psbt.py akan diskalakan ke max_fee_atomic = 150000 (melebihi batas kebijakan 100000), sehingga menghasilkan payload berikut yang memicu keputusan evaluasi kebijakan 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

Hasil Evaluasi Kebijakan yang Diharapkan: decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED).

✅ Kasus B: Persetujuan (PSBT Bitcoin yang Diizinkan)

Buat transaksi dengan PSBT valid yang dibuat sebelumnya (RECIPIENT_SATS = 50000 dipetakan ke amount_token_scaled = 500000 <= 2500000 dan MINER_FEE_SATS = 250 dipetakan ke 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')

Respons yang Seharusnya: state: PENDING_AUTHORIZATION dan policyEvaluationRecord.decision: PERMIT.

Bitcoin, OpenSSL

Dalam model UTXO Bitcoin, transaksi membelanjakan output tertentu yang belum dibelanjakan. Di SegWit (BIP 143), penghitungan hash tanda tangan memerlukan pengetahuan tentang jumlah satoshi dan skrip penguncian input yang dibelanjakan.

Evaluasi kebijakan Bitcoin memerlukan format BIP 174 PSBT v0 (BITCOIN_TX_V2) dengan PSBT_IN_WITNESS_UTXO yang diisi untuk setiap input.

Skrip Konstruksi PSBT (build_psbt.py)

Jalankan blok berikut untuk menyusun payload PSBT v0 BIP 174 yang valid:

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"

🛑 Kasus A: Penolakan (Batas Biaya Terlampaui)

Mencoba membuat transaksi dengan biaya penambang yang melebihi batas kebijakan 10.000 satoshi. Dalam contoh ini, menetapkan MINER_FEE_SATS = 15000 (dan CHANGE_SATS = 135000) di build_psbt.py akan diskalakan ke max_fee_atomic = 150000 (melebihi batas kebijakan 100000), sehingga menghasilkan payload berikut yang memicu keputusan evaluasi kebijakan 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

Hasil Evaluasi Kebijakan yang Diharapkan: decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED).

✅ Kasus B: Persetujuan (PSBT Bitcoin yang Diizinkan)

Buat transaksi dengan PSBT valid yang dibuat sebelumnya (RECIPIENT_SATS = 50000 dipetakan ke amount_token_scaled = 500000 <= 2500000 dan MINER_FEE_SATS = 250 dipetakan ke 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')

Respons yang Seharusnya: state: PENDING_AUTHORIZATION dan policyEvaluationRecord.decision: PERMIT.

🕵️‍♂️ Memeriksa Status Transaksi (Polling)

Lakukan polling status transaksi dengan menjalankan perintah berikut secara berkala:

curl

curl -s -X GET \
  -H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${TRANSACTION_NAME}" | jq

B. Mencatat Keputusan Penanda Tangan (Memberi Otorisasi)

Karena CreateTransaction di Langkah 6.A telah mencatat keputusan AUTHORIZE Penanda Tangan 1 (1 dari 2 yang diperlukan oleh nilai minimum KeyPolicy 2 dari 3), hanya satu penanda tangan bersama tambahan yang memiliki key share yang perlu mengizinkan transaksi menggunakan transactions.recordSignerDecision (RecordSignerDecision).

Tanda tangani TransactionSigningPayload yang menyebutkan transaction (${TRANSACTION_NAME}) dan panggil recordSignerDecision sebagai Penanda Tangan 2 (bob@example.com):

curl

# 1. Check state for Signer 2
TX_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth print-access-token "bob@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${TRANSACTION_NAME}")

TX_STATE=$(echo "$TX_INFO" | jq -r '.state // "UNKNOWN"')

if [ "$TX_STATE" == "PENDING_AUTHORIZATION" ]; then
  # 2. Sign as Signer 2 naming transaction ($TRANSACTION_NAME)
  SIGNER_2_SIG_B64=$(sign_tx_decision "signer-bob" "transaction" "${TRANSACTION_NAME}" "AUTHORIZE" "${DIGEST_HEX}")
  SIGNER_2_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)

  # 3. Call RecordSignerDecision as Signer 2
  echo "Recording AUTHORIZE decision for Signer 2..."
  curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "bob@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d "{
      \"decision\": \"AUTHORIZE\",
      \"authorizationInfo\": {
        \"signature\": \"${SIGNER_2_SIG_B64}\",
        \"signedPayload\": \"${SIGNER_2_PAYLOAD_B64}\"
      }
    }" \
    "${INSTANCE_URL}/v1beta/${TRANSACTION_NAME}:recordSignerDecision" | jq
elif [ "$TX_STATE" == "SIGNING" ] || [ "$TX_STATE" == "SIGNING_SUCCEEDED" ]; then
  echo "Transaction is in state $TX_STATE. Skipping Signer 2."
else
  echo "Transaction is in an unexpected state: $TX_STATE"
fi

C. Mengambil Tanda Tangan Akhir

Setelah nilai minimum otorisasi tercapai, periksa status transaksi dan ambil finalSignature (dan recoveryId untuk kunci 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

Jika state adalah SIGNING_SUCCEEDED, salin kolom finalSignature dari JSON yang ditampilkan untuk Langkah 7.


Langkah 7: Menyiarkan Transaksi ke Blockchain

Setelah transaksi Anda mencapai status SIGNING_SUCCEEDED dan Anda telah mengambil finalSignature, langkah terakhir adalah membuat transaksi wire yang ditandatangani dan menyiarkannya ke jaringan.

Ethereum

Derivasi & Penyiaran ID Pemulihan

MPC API menampilkan tanda tangan ECDSA 64 byte mentah di finalSignature (terdiri dari nilai r dan s 32 byte dalam bentuk low-S kanonis per EIP-2), bersama dengan bit paritas pemulihan (0 atau 1) di recoveryId.

Skrip Python berikut membuat transaksi yang ditandatangani menggunakan finalSignature Anda, memverifikasi alamat pengirim yang dipulihkan terhadap alamat Ethereum kunci MPC Anda, dan menyiarkannya ke jaringan.

Skrip Siaran:

Jalankan blok yang dapat disalin dan ditempel berikut di terminal Anda untuk menulis dan menjalankan 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

Memecahkan Masalah Umum

  • invalid sender: recovery failed atau invalid transaction v, r, s values: Hal ini terjadi jika parameter transaksi dalam skrip Anda tidak sama persis dengan payload yang ditandatangani. Pastikan kolom value, nonce, dan to cocok dengan yang diproses di Langkah 6.
  • insufficient funds for gas * price + value: Transaksi Anda valid secara kriptografis dan diterima oleh node, tetapi alamat Ethereum Kunci MPC Anda tidak memiliki cukup ETH testnet untuk mencakup operasi. Anda harus mendanai alamat Anda sebelum melakukan siaran.

Bitcoin

Perakitan & Penyiaran Transaksi Wire SegWit

Untuk transaksi Bitcoin Native SegWit (BIP 141), tanda tangan MPC (r, s) 64 byte mentah harus diformat menjadi ASN.1 DER low-S kanonis, ditambahkan dengan 0x01 (SIGHASH_ALL), dan digabungkan dengan kunci publik terkompresi 33 byte ke dalam stack witness SegWit 2 elemen: [DER signature || 0x01, compressed public key].

Skrip Siaran:

Jalankan blok berikut untuk membuat dan menjalankan 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

Memecahkan Masalah Umum

  • mandatory-script-verify-flag-failed (Signature must be zero for failed CHECK(MULTI)SIG operation) / non-mandatory-script-verify-flag (Non-canonical DER signature): Pastikan komponen s tanda tangan dalam format low-s kanonis (s <= N/2) dan dienkode DER dengan benar.
  • bad-txns-in-belowout / bad-txns-fee-negative: Total output melebihi nilai input. Pastikan penghitungan perubahan (InputSatoshis - PaymentSatoshis - MinerFee) Anda benar-benar positif.

📖 Lampiran: Cookbook Operasional (Membuat Kueri & Memeriksa)

Gunakan perintah ini untuk memeriksa dan mengelola status resource Anda kapan saja.


🏢 1. Instance

Memeriksa dan mengelola instance MPC di project dan lokasi Anda.

A. Mencantumkan semua Instance

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "https://mpc.googleapis.com/v1beta/projects/my-project/locations/us-central1/instances" | jq

B. Mendapatkan detail untuk Instance tertentu

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. Tim & Pengguna

Memeriksa tim dan pengguna di instance Anda.

A. Mencantumkan semua Tim

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/teams" | jq

B. Mendapatkan detail untuk Tim tertentu

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${TEAM_NAME}" | jq

C. Menampilkan daftar semua Pengguna terdaftar

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/users" | jq

D. Mendapatkan detail untuk Pengguna tertentu

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. Kebijakan Transaksi & KeyList

Memeriksa kebijakan transaksi dan daftar kunci.

A. Mencantumkan semua Kebijakan Transaksi

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/transactionPolicies" | jq

B. Mendapatkan detail untuk Kebijakan Transaksi tertentu

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${POLICY_NAME}" | jq

C. Mencantumkan dan Mendapatkan KeyList

curl

# List all KeyLists
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/keyLists" | jq

# Get a specific KeyList
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${KEY_LIST_NAME}" | jq

🔑 4. Kunci & KeyRing

Memeriksa key ring dan kunci yang didistribusikan.

A. Membuat daftar semua KeyRing

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/keyRings" | jq

B. Mencantumkan semua Kunci dalam 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

C. Mendapatkan detail Kunci Terdistribusi tertentu

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. Transaksi (Operasi Penandatanganan)

Memeriksa transaksi dan status penandatanganan.

A. Mencantumkan semua Transaksi untuk sebuah Kunci (atau di semua Kunci)

curl

# List transactions for a specific Key
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions" | jq

# List transactions across all KeyRings and Keys using the '-' wildcard
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/keyRings/-/keys/-/transactions" | jq

B. Mendapatkan detail atau status Transaksi tertentu

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