Skip to content

fix: compare INDEX ownership by key, not by rendered address - #26

Merged
DHEBP merged 1 commit into
DHEBP:devfrom
chakipu2:fix/index-owner-network-rendering
Aug 29, 2026
Merged

fix: compare INDEX ownership by key, not by rendered address#26
DHEBP merged 1 commit into
DHEBP:devfrom
chakipu2:fix/index-owner-network-rendering

Conversation

@chakipu2

Copy link
Copy Markdown
Collaborator

What this fixes

On the simulator, updating a TELA INDEX is impossible. Every attempt is refused with:

Your wallet is not the owner of this INDEX. Only the original author can update it.

including from the wallet that installed the INDEX a minute earlier. GetINDEXInfo returns isOwner: false for the same reason, so the interface also shows an owner their own INDEX as not theirs.

How I ran into it

Developing a TELA app against HOLOGRAM's simulator — a set of DOCs under one INDEX. One of those DOCs has to carry the INDEX's own SCID as a constant, so that the DOC can point back at the application it belongs to. That SCID does not exist until the DOCs are installed, so the constant can only be filled in afterwards. The loop is: install the DOCs, install the INDEX, note its SCID, write it into that one DOC, redeploy that DOC, update the INDEX.

The last step never worked — not once, with any wallet. Since being repointable is the entire purpose of an INDEX, the only workaround was to leave that constant empty on the simulator and fill it in at mainnet deployment. That means shipping to mainnet the one step that had never been exercised, which is exactly backwards.

Root cause

The two strings being compared are produced by two different renderers, and they only agree on mainnet.

What the INDEX stores. TELA-INDEX-1.bas does STORE("owner", address()), where address() is ADDRESS_STRING(SIGNER()). In derohe, dvm_address_string (dvm/dvm_functions.go) builds that string as:

addr := rpc.NewAddressFromKeys(p)
return true, addr.String()

rpc.NewAddressFromKeys sets Mainnet: true, and nothing resets it. That is deliberate and correct — consensus cannot depend on which network a node believes it is running on — so a contract records dero1… on every chain, the simulator included.

What the wallet renders. wallet.GetAddress().String() goes through walletapi using the wallet's own network flag, which HOLOGRAM sets from IsInSimulatorMode(). On the simulator that is deto1….

A DERO address is bech32: the human-readable prefix names the network, and the six-character checksum is computed over that prefix. So one key renders as two strings that differ at both ends:

stored by the contract  dero1qyw4fl3dupcg5qlrcsvcedze507q9u67lxfpu8kgnzp04aq73yheqqg2ctjn4
rendered by the wallet  deto1qyw4fl3dupcg5qlrcsvcedze507q9u67lxfpu8kgnzp04aq73yheqqgsph2ka
                          ^                                                         ^^^^^^

(Vector taken from this repo's own network_mismatch_guard_test.go; the second line is the first re-rendered with Mainnet = false.) Everything between those two marks is identical, and both strings decode to the same compressed point. existingIndex.Author != walletAddr is therefore true forever on the simulator.

The contract's own gate was never affected. UpdateCode checks IF LOAD("owner") == address() — one DVM rendering against another, always consistent. The chain would have accepted every one of these updates. Only the client-side pre-check refused them.

This is also why tela-cli refuses the same operation: it carries the identical comparison (see Not addressed here).

The change

tela_service.go only, no new imports (rpc was already imported):

  • New helper sameINDEXAuthor(stored, walletAddr string) bool — decodes both strings with rpc.NewAddress and compares Compressed(), the 33-byte public key.
  • UpdateINDEX uses it instead of !=, and logs both values when it refuses, so the next refusal is diagnosable instead of opaque.
  • GetINDEXInfo uses it for isOwner.

New file tela_index_owner_test.go covering the helper.

Why compare keys rather than re-render both on one network

Normalising both to one network works too (BaseAddress(), set Mainnet, compare strings). I chose the key because re-rendering makes the check depend on getting a network flag right at the moment of comparison — which is the class of bug being fixed here. Compressed() returns the identity the contract is actually gating on, and it is incidentally indifferent to an integrated address, since a payment ID says nothing about ownership.

Scope and risk

  • Mainnet behaviour is unchanged. Both renderings already agreed there, and the stored == walletAddr fast path returns before any decoding happens.
  • Not more permissive in any way that matters. The compressed public key is the identity, and it is what the contract gates on through address(). Nothing that was refused for a real reason is now accepted.
  • Fails closed. An author string that does not parse returns false. The "anon" (immutable, ring 16+) case is caught by the existing check above this one and would return false here regardless.
  • One new console line on refusal, printing both strings. The old failure was unreadable — the user is told they are not the owner and given no way to find out why. Happy to drop it if you would rather not have addresses in the console log.
  • No frontend, no design-system, no dependency changes.

Not addressed here

  • tela-cli has the identical defect, in four places — index.Author != app.wallet.disk.GetAddress().String() in cmd/tela-cli/main.go. That is upstream in civilware/tela and out of scope for this repo per CONTRIBUTING. Mentioned so the same symptom reported from the CLI is not mistaken for this fix failing.
  • tela.Updater performs no ownership check of its own — I checked. The gate is entirely client-side, so nothing in the TELA library needs to change for this to work.
  • HOLOGRAM's two wallet-selection policies on the simulator. InstallSmartContract always takes wallet #0 while getWalletForDeployment (used by InstallDOC, InstallINDEX, UpdateINDEX, DeployTELABatch) prefers the wallet open in the interface, with nothing on screen saying so. A separate, real inconsistency I ran into while chasing this; not touched here. Happy to open an issue if it is useful.

Testing

tela_index_owner_test.go is a table test over: identical renderings (mainnet and simulator), both mainnet/simulator cross-pairs, a different wallet in both networks, "anon", empty strings on either side, an unparseable author, and an integrated address against its base address. The simulator vectors are derived at test time by flipping Mainnet on a mainnet vector, so there are no hardcoded deto1 constants to rot.

Also run locally: gofmt -l, go vet ./..., go build ./..., go test -short ./... (matching CI).

Manually, on a fresh simulator: a full set of DOCs and an INDEX installed, one DOC redeployed with the INDEX SCID baked into it, then the INDEX updated — accepted, broadcast and mined, and the INDEX now serving the new DOC. The same sequence was refused at the client before this change.

AI disclosure

Per CONTRIBUTING: this patch was written with an AI coding agent (Claude). The root cause above was read out of the actual sources — derohe dvm/dvm_functions.go, civilware/tela TELA-INDEX-1.bas and tela.go — rather than inferred, and the unit test was compiled and run against the pinned DHEBP/derohe revision from go.mod.


Based on dev @ 827de4e (v1.0.8).

The author stored in a TELA INDEX comes from the DVM's ADDRESS_STRING, which
builds it with rpc.NewAddressFromKeys and leaves Mainnet true on every chain --
consensus cannot depend on which network a node thinks it is on. walletapi
renders the same key through the wallet's network flag, so on the simulator it
says deto1... A DERO address is bech32, prefix and checksum both carry the
network, so the two strings never matched and update-index was refused on the
simulator, always, including for the wallet that installed the INDEX.

Decide ownership on the decoded public key instead. The contract's own gate
compares address() to address() and was never affected; mainnet, where both
renderings agree, is unchanged.

Also applies the same comparison to the isOwner flag in GetINDEXInfo, and logs
both values when an update is refused.

@DHEBP DHEBP left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is right, merging. Good catch that ADDRESS_STRING is the source, I'd have gone looking at the wallet renderer first.

I reverted sameINDEXAuthor to a plain string compare to check your test, and it fails on both cross-network cases and the integrated one. It's pinning the fix.

Outside this PR: content_filter.go:533 does the same bare compare for author filter rules, so a rule written with a deto1 address never matches an app storing dero1. I'll file that separately.

@DHEBP
DHEBP merged commit 0b20883 into DHEBP:dev Aug 29, 2026
2 checks passed
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