Skip to content

fix: sign a dApp's contract deployment with the connected wallet - #29

Open
chakipu2 wants to merge 1 commit into
DHEBP:devfrom
chakipu2:fix/xswd-sc-deploy-signer
Open

fix: sign a dApp's contract deployment with the connected wallet#29
chakipu2 wants to merge 1 commit into
DHEBP:devfrom
chakipu2:fix/xswd-sc-deploy-signer

Conversation

@chakipu2

Copy link
Copy Markdown
Collaborator

What this fixes

Two defects on the path a dApp uses to deploy a contract — transfer carrying an sc field, the form Engram and the TELA CLI accept.

On the simulator, the deployment is signed by the wrong wallet. Whatever wallet is open in the interface, the contract is signed by wallet #0. A contract that records its own deployer — STORE("owner", SIGNER()), the ordinary pattern — therefore records a wallet the dApp was never connected to, and refuses its own creator on the next call.

Off the simulator, the same path deadlocks the wallet. It never returns, and every later wallet operation blocks behind it until HOLOGRAM is restarted.

How I ran into it

A TELA dApp connected over XSWD, deploying a contract whose Initialize stores its deployer. Every other call on that same connection — GetAddress, GetBalance, scinvoke — answered for the wallet I had open. Only the deployment did not: the owner the contract recorded was wallet #0's key, and the dApp could no longer act on the contract it had just created. The same gesture through Engram, which forwards transfer to the connected wallet, records the right key.

Root cause

1. The sc branch throws away the wallet the call already resolved

The XSWD server has no wallet selection of its own. It delegates: after approval, xswd_server.go:1174 calls s.app.InternalWalletCall(...), which takes walletManager.Lock() and sets wallet := walletManager.wallet — the wallet open in the interface (wallet.go:1982-2014). GetAddress (:2019), scinvoke (:2312) and the value-transfer path (:2088+) all use that wallet. Hence the consistency everywhere else.

The sc branch (wallet.go:2091-2105) is the sole exception. It discards that resolved wallet and calls InstallSmartContract, which decides again on its own (sc_function_parser.go:463-479):

if isSimulator {
    wallet = a.simulatorManager.walletManager.GetPrimaryWallet()   // wallet #0
} else {
    wallet = GetWallet()
}

GetPrimaryWallet() is GetInternalWallet(0) (simulator_wallets.go:537-540). One line contradicts the wallet the rest of the session uses.

2. Off the simulator, that same handoff cannot return

InternalWalletCall holds walletManager.Lock() for its whole body (wallet.go:1983-1984; the comment at xswd_permissions.go:128 says so explicitly). Off simulator, InstallSmartContract calls GetWallet(), which takes walletManager.RLock() (wallet.go:1613-1620).

sync.RWMutex is not reentrant: a goroutine already holding the write lock can never acquire the read lock. I reproduced exactly that sequence in a minimal program — it never returns. The deferred Unlock() therefore never runs, the wallet manager stays write-locked, and the whole wallet interface freezes until restart.

So "the two paths converge off simulator and nothing shows on mainnet" is not true: off simulator this path does not sign with the wrong wallet, it does not sign at all. That nobody has reported it suggests no mainnet dApp has yet deployed by transfer from HOLOGRAM.

3. The funding rationale for wallet #0 does not hold

The comment at sc_function_parser.go:468-471 justifies the primary wallet by saying the open wallet "may not have a properly synced balance in simulator mode". The chain says otherwise:

  • Every registered wallet is credited the same. derohe/blockchain/transaction_execute.go, case transaction.REGISTRATION: if !globals.IsMainnet() { zerobalance.Plus(800000) }. HOLOGRAM registers all of its pre-seeded wallets at startup (simulator_manager.go, RegisterAllWallets).
  • None of them is premined. I derived the public keys from the seeds in simulator_wallets.go and searched derohe/premine/list.txt (1326 accounts): none appears. Positive control on three real entries from that file, decoded with the transaction package — those are found.
  • Mining does not pay wallet #0. cmd/simulator/simulator.go:206, 225: the initial block and mine_block_auto pay genesis_wallet, a separate seed HOLOGRAM does not expose.

So on the simulator, wallet #0 holds no funding advantage over any other. Off simulator, where a "the open wallet may be empty" argument could carry weight, the primary-wallet branch never applies.

4. Nothing on screen said which wallet would sign

xswd_server.go:1043 emitted xswd:request with method, params, appName and origin — no signer address. App.svelte built the modal payload from transfers, scid, entrypoint, deposits and fees — the sc field was not among them. A transfer carrying sc has no destination, no SCID and no entrypoint, so the approval prompt showed an empty request while a contract was about to be deployed under a signature the user could not see.

A wallet nobody chose, signing in silence, is worse than the wrong choice itself.

The change

One commit, eight files.

  1. Core split from selection (sc_function_parser.go): installSmartContractWith(wallet, code, anonymous) signs with the wallet it is given and never touches walletManager. InstallSmartContract keeps the selection for callers that have no wallet in hand.
  2. The sc branch (wallet.go) passes the wallet it already resolved. That single change removes both the wrong signer and the deadlock.
  3. One policy for deployment signing: InstallSmartContract, DeployToSimulator and deployRawSC all resolve through getWalletForDeployment — open wallet first, primary as a fallback — which is what every TELA deployment already did. deployRawSC takes its signer as a parameter.
  4. Disclosure: the signing-request event carries the signer address and flags a deployment with the size of the code; App.svelte forwards them; WalletModal.svelte shows "CONTRACT DEPLOYMENT" and "SIGNING WALLET".
  5. xswd_sc_deploy_signer_test.go: three source sentinels (the sc branch passes its wallet and no longer calls the selecting entry point; the core touches neither walletManager, nor GetWallet(), nor GetPrimaryWallet(; no deployment path takes the primary wallet directly), one behaviour test on the policy with a real Wallet_Disk, and two tests on the approval event.

Scope and risk

  • The signer only ever becomes the wallet the user has open. Where no wallet is open, the primary is still the fallback — the same rule TELA deployments have always used, so this aligns the odd path with the established one rather than inventing a policy.
  • The deadlock fix is the same edit, not an extra one: the core no longer reaches for the wallet manager, so there is no second lock acquisition to deadlock on.
  • Frontend changes are additive and reuse existing markup. The two new rows use the existing modal-tx-field / modal-tx-label / modal-tx-scid pattern — no new colors, spacing, class names or animations, per the Design System Rulebook.
  • BatchDeployToSimulator and the Studio deploy screen now resolve their signer the same way. That is a deliberate behaviour change on the simulator: a deploy from Studio is signed by the wallet you have open rather than by #0. It is the point of the fix, but flagging it since it is visible beyond the XSWD path.

Testing

go build ./..., go vet ./..., go test -short ./..., gofmt -l — clean. npm run build for the frontend — clean.

The three source sentinels were replayed against the unfixed tree: all three fail there, so they are testing what they claim to test.

Manually, on the simulator: a wallet other than #0 open, a dApp connected to it over XSWD deploying a contract whose Initialize does STORE("owner", HEX(SIGNER())), then reading the stored key back with GetSC. Before the change the stored key is wallet #0's; after it, the connected wallet's.

AI disclosure

Per CONTRIBUTING: this patch was written with an AI coding agent (Claude). The call paths, the lock behaviour and the premine/registration evidence above were read out of the actual sources — wallet.go, sc_function_parser.go, xswd_permissions.go, derohe/blockchain/transaction_execute.go, derohe/premine/list.txt, cmd/simulator/simulator.go — rather than inferred, and the deadlock was confirmed with a standalone program reproducing the lock sequence.

A dApp can deploy by sending transfer with an "sc" field, the way Engram and the
TELA CLI accept it. InternalWalletCall resolves the connected wallet, then handed
that field to InstallSmartContract, which decided the signer again on its own. In
simulator mode it took the primary wallet, so a contract that records its deployer
- STORE("owner", SIGNER()), the ordinary pattern - recorded a wallet the dApp had
never connected to, while GetAddress, GetBalance and scinvoke on the same
connection all answered for the open wallet. The contract then refused its own
creator on the next call, and nothing on screen had said which wallet signed.

Off simulator the same handoff called GetWallet(), which takes walletManager's
read lock, from inside InternalWalletCall, which holds its write lock for the
whole body. sync.RWMutex is not reentrant: that read lock can never be taken by
the goroutine already holding the write lock, so the call never returned, the
deferred unlock never ran, and every later wallet operation blocked behind it.

Split the install into a core that signs with a caller-supplied wallet and takes
no wallet decision - it never reaches walletManager, so it can neither substitute
a signer nor re-enter the lock - and a selection step for callers that have no
wallet in hand. The XSWD branch passes the wallet it already resolved. The Studio
deploy screen and the simulator developer deploy now resolve their signer through
getWalletForDeployment, the policy every TELA deployment already used: the open
wallet, with the primary as a fallback when none is open.

The rule that was there before was justified by funding - the open wallet may
hold nothing. On the simulator that does not hold: derohe credits every wallet
the same amount at registration (transaction_execute.go, REGISTRATION case, on
any non-mainnet chain), none of the pre-seeded simulator wallets appears in the
premine list, and the simulator mines to its own genesis wallet, which is not one
of them. Wallet #0 has no funding advantage over the others. Off simulator the
rule never applied, since that path already used the open wallet.

Also names the signer in the approval prompt. A transfer carrying "sc" has no
destination, no SCID and no entrypoint, so it reached the modal as an empty
request while a contract was about to be deployed; the event now carries the
signing address and marks the deployment with the size of its code.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants