Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 71 additions & 32 deletions cmd/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,19 +205,12 @@ func runVerifyPublicChain() error {
}
fmt.Printf(" public chain id: %s\n", chainID)

userIDBytes := make([]byte, 32)
if _, err := rand.Read(userIDBytes); err != nil {
return fmt.Errorf("rand: %w", err)
}
userID := "0x" + hex.EncodeToString(userIDBytes)

// Steps 2-3 are idempotent by intent and EXPECTED to fail on stacks whose
// deploy pre-registers the deployer (the 3.0.1 deploy does:
// deploy pre-registers the deployer (some deploys do:
// RNUserGovernanceV1__PublicAddressAlreadyMapped) — run them silently, name
// the KNOWN benign outcomes in one line, and surface anything else (the
// tasks swallow their own errors and exit 0, so the captured output is the
// only evidence there is). Both steps continue either way: the bridge's
// final balance poll is the ground truth.
// only evidence there is).
reportIdempotentStep := func(out string, err error, benignMarkers []string, benignNote string) {
failure := contractsTaskFailed(out, err)
if failure == nil {
Expand All @@ -229,33 +222,79 @@ func runVerifyPublicChain() error {
return
}
}
yellow.Printf(" step did not succeed (continuing — the bridge outcome below is the ground truth):\n %s\n", outputTail(out, 4))
yellow.Printf(" step did not succeed:\n %s\n", outputTail(out, 4))
}

bold.Println(">> [2/8] Registering deployer as a user (idempotent)")
out2, err2 := stacks.ExecContractsCaptureSilent([]string{
"npx", "hardhat", "createUser",
"--pn", "A",
"--user-id", userID,
"--public-address", verifyDeployerAddress,
"--private-address", verifyDeployerAddress,
})
// 0x609fe1e4 is the RNUserGovernanceV1__PublicAddressAlreadyMapped()
// selector: the same benign outcome, seen raw when the task's ethers call
// cannot decode the custom error.
reportIdempotentStep(out2, err2,
[]string{"PublicAddressAlreadyMapped", "already registered", "already exists", "0x609fe1e4"},
"deployer address already registered — continuing (expected: the contracts deploy pre-registers it)")
registerDeployer := func() error {
userIDBytes := make([]byte, 32)
if _, err := rand.Read(userIDBytes); err != nil {
return fmt.Errorf("rand: %w", err)
}
userID := "0x" + hex.EncodeToString(userIDBytes)

out2, err2 := stacks.ExecContractsCaptureSilent([]string{
"npx", "hardhat", "createUser",
"--pn", "A",
"--user-id", userID,
"--public-address", verifyDeployerAddress,
"--private-address", verifyDeployerAddress,
})
// 0x609fe1e4 is the RNUserGovernanceV1__PublicAddressAlreadyMapped()
// selector: the same benign outcome, seen raw when the task's ethers call
// cannot decode the custom error.
reportIdempotentStep(out2, err2,
[]string{"PublicAddressAlreadyMapped", "already registered", "already exists", "0x609fe1e4"},
"deployer address already registered — continuing (expected when the contracts deploy pre-registers it)")

out3, err3 := stacks.ExecContractsCaptureSilent([]string{
"npx", "hardhat", "approveUser",
"--pn", "A",
"--user-id", userID,
})
reportIdempotentStep(out3, err3,
[]string{"has no address pairs", "already approved", "User does not exist"},
"approval not needed for this user — continuing (the deployer's mapping is already active)")
return nil
}

bold.Println(">> [2/8] Registering deployer as a user (idempotent)")
bold.Println(">> [3/8] Approving the user")
out3, err3 := stacks.ExecContractsCaptureSilent([]string{
"npx", "hardhat", "approveUser",
"--pn", "A",
"--user-id", userID,
})
reportIdempotentStep(out3, err3,
[]string{"has no address pairs", "already approved", "User does not exist"},
"approval not needed for this user — continuing (the deployer's pre-registered mapping is already active)")
// The teleport in step 8 hard-requires the deployer to be a registered AND
// approved user on PN A (RNUserGovernanceV1__PrivateAddressNotMapped
// otherwise), and the registration tasks above tolerate benign
// already-mapped outcomes — so a transiently failed registration (seen live:
// a nonce race when verify runs right after init, while the deploy's
// background add-authorized-relayers txs from the SAME system account are
// still settling) must be caught HERE, not two minutes later as an opaque
// revert after the token deploy. Assert the mapping on-chain and retry the
// registration a few times before proceeding.
deployerApproved := false
for attempt := 1; attempt <= 3; attempt++ {
if attempt > 1 {
yellow.Printf(" deployer mapping not active yet — retrying registration (attempt %d/3)\n", attempt)
time.Sleep(5 * time.Second)
}
if err := registerDeployer(); err != nil {
return err
}
approved, checkErr := stacks.DeployerApprovedOnPNA(verifyDeployerAddress)
if checkErr != nil {
// The assertion exists to catch registration races, not to add its
// own failure mode: if the check itself can't run (RPC unreachable
// from the host, unexpected .env), warn and trust the tasks' output.
yellow.Printf(" could not verify the deployer registration on-chain (%v) — continuing on the registration tasks' word\n", checkErr)
deployerApproved = true
break
}
if approved {
deployerApproved = true
break
}
}
if !deployerApproved {
return fmt.Errorf("the deployer %s is not registered/approved on PN A after 3 attempts — the bridge in step 8 cannot work.\nInspect the registration tasks manually: docker exec <contracts> npx hardhat createUser --pn A ... (see `rayls logs contracts`)", verifyDeployerAddress)
}
green.Println(" deployer registration confirmed on-chain (approved address pair on PN A)")

suffixBytes := make([]byte, 3)
if _, err := rand.Read(suffixBytes); err != nil {
Expand Down
70 changes: 70 additions & 0 deletions internal/stacks/verifyregistration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package stacks

import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"

"golang.org/x/crypto/sha3"
)

// pnARPCURL is PN A's EVM JSON-RPC as published on the host: every generated
// compose maps privacy-node-a (participant index 0) to 127.0.0.1:8545.
const pnARPCURL = "http://127.0.0.1:8545"

// DeployerApprovedOnPNA reports whether the given address has an APPROVED
// address pair in PN A's RNUserGovernanceV1 — the hard precondition of
// teleportToPublicChain. The governance proxy address exists only inside the
// contracts container (the deploy writes it into the image's .env), so it is
// read with an in-container grep; the check itself is a raw eth_call from the
// host, keeping the image's node/ethers runtime out of the loop. The contract
// reverts (PrivateAddressNotMapped) for unknown addresses, so an RPC-level
// error reads as "not approved" rather than a hard failure — the caller
// retries; only a broken transport/setup surfaces as err.
func DeployerApprovedOnPNA(deployer string) (bool, error) {
out, err := ExecContractsCaptureSilent([]string{"grep", "-m1", "^PRIVACY_NODE_A_RAYLS_NODE_USER_GOVERNANCE=", ".env"})
if err != nil {
return false, fmt.Errorf("reading the PN A user-governance address from the contracts container: %w", err)
}
_, govAddr, found := strings.Cut(strings.TrimSpace(out), "=")
if !found || !strings.HasPrefix(govAddr, "0x") {
return false, fmt.Errorf("unexpected PRIVACY_NODE_A_RAYLS_NODE_USER_GOVERNANCE line %q in the contracts .env", strings.TrimSpace(out))
}

// checkUserIsApprovedByPrivateAddress(address): 4-byte selector
// (0xc4ba03ed) followed by the address as one left-padded 32-byte word.
h := sha3.NewLegacyKeccak256()
h.Write([]byte("checkUserIsApprovedByPrivateAddress(address)"))
callData := "0x" + hex.EncodeToString(h.Sum(nil)[:4]) +
strings.Repeat("0", 24) + strings.ToLower(strings.TrimPrefix(deployer, "0x"))

reqBody, _ := json.Marshal(map[string]any{
"jsonrpc": "2.0", "id": 1,
"method": "eth_call",
"params": []any{map[string]string{"to": govAddr, "data": callData}, "latest"},
})
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Post(pnARPCURL, "application/json", bytes.NewReader(reqBody))
if err != nil {
return false, err
}
defer resp.Body.Close()
var rpcOut struct {
Result string `json:"result"`
Error *struct {
Message string `json:"message"`
} `json:"error"`
}
if err := json.NewDecoder(resp.Body).Decode(&rpcOut); err != nil {
return false, err
}
if rpcOut.Error != nil {
// Reverts land here: the deployer is simply not mapped/approved yet.
return false, nil
}
return strings.HasSuffix(rpcOut.Result, "1"), nil
}
Loading