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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

---

## [Unreleased]

### Fixed
- **A TELA INDEX can be updated on the simulator again.** `update-index` refused every attempt with *"Your wallet is not the owner of this INDEX"*, including from the wallet that had just installed it. The check compared the author string stored in the INDEX against the address the wallet renders — but those two strings come from two different renderers. The stored author is what the contract's `address()` returned, which is the DVM's `ADDRESS_STRING`, which builds it with `rpc.NewAddressFromKeys` and leaves `Mainnet` true; consensus cannot depend on which network a node thinks it is on, so a contract records `dero1...` on every chain, the simulator included. walletapi renders the same key through the wallet's network flag and says `deto1...` there. A DERO address is bech32 — the prefix names the network and the checksum is computed over it — so both halves differ and the compare read one wallet as two, always. Ownership is now decided on the decoded public key. The contract's own owner gate was never affected (it compares `address()` to `address()`), and neither was mainnet, where both renderings agree. The same correction applies to the ownership flag reported by INDEX info, which read `false` for the owner's own INDEX on the simulator.

## [1.0.8] - 2026-08-20

A rebuilt consent model for dApps — connecting no longer asks for wallet access, and access that is remembered is remembered per wallet. Plus sender-attribution privacy, cold-wallet genesis, XSWD/Browser trust-boundary hardening, fund-safety guards on the hot send path, Linux launch fixes for WebKitGTK, TELA file-drop, name resolution, and a required consensus repin ahead of the DERO network's HF3 activation.
Expand Down
90 changes: 90 additions & 0 deletions tela_index_owner_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright 2025 HOLOGRAM Project. All rights reserved.
// Regression test for the INDEX ownership check in UpdateINDEX.

package main

import (
"testing"

"github.com/deroproject/derohe/rpc"
)

// renderForSimulator re-renders a mainnet address vector (dero1...) for the
// simulator network (deto1...) without touching the key it wraps. A wallet opened
// on a simulator renders its own address this way, while the author the INDEX
// stored came from the DVM and is always in the dero1 form.
func renderForSimulator(t *testing.T, mainnet string) string {
t.Helper()
addr, err := rpc.NewAddress(mainnet)
if err != nil {
t.Fatalf("parse vector %s: %v", mainnet, err)
}
addr.Mainnet = false
return addr.String()
}

// TestINDEXAuthorMatchesAcrossNetworkRendering locks in the fix for the defect that
// made update-index impossible on a simulator: the stored author and the wallet
// address are the same key rendered on two networks, and a string compare reads them
// as two different wallets. Both prefix and checksum differ, so the mismatch is total
// and the refusal was systematic.
func TestINDEXAuthorMatchesAcrossNetworkRendering(t *testing.T) {
const owner = "dero1qyw4fl3dupcg5qlrcsvcedze507q9u67lxfpu8kgnzp04aq73yheqqg2ctjn4"
const stranger = "dero1qy976ssakhfynpd4lnh39u7gw9spfzr9z55ckfd0yhrhsdr235glgqq28xlvm"

ownerSim := renderForSimulator(t, owner)
strangerSim := renderForSimulator(t, stranger)

if ownerSim == owner {
t.Fatal("simulator rendering is identical to the mainnet one; the vector says nothing")
}

cases := []struct {
name string
stored string
walletAddress string
want bool
}{
{"same rendering", owner, owner, true},
{"stored mainnet, wallet simulator", owner, ownerSim, true},
{"stored simulator, wallet mainnet", ownerSim, owner, true},
{"same rendering, simulator", ownerSim, ownerSim, true},
{"different wallet, same network", owner, stranger, false},
{"different wallet, across networks", owner, strangerSim, false},
{"immutable INDEX", "anon", owner, false},
{"empty stored author", "", owner, false},
{"empty wallet address", owner, "", false},
{"unparseable stored author", "not-an-address", owner, false},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := sameINDEXAuthor(tc.stored, tc.walletAddress); got != tc.want {
t.Fatalf("sameINDEXAuthor(%q, %q) = %v, want %v", tc.stored, tc.walletAddress, got, tc.want)
}
})
}
}

// TestINDEXAuthorIgnoresIntegratedPayload covers the case where one side carries an
// integrated payload: it is the same wallet, and an update must not be refused for a
// payment ID that has nothing to do with ownership.
func TestINDEXAuthorIgnoresIntegratedPayload(t *testing.T) {
const owner = "dero1qyw4fl3dupcg5qlrcsvcedze507q9u67lxfpu8kgnzp04aq73yheqqg2ctjn4"

addr, err := rpc.NewAddress(owner)
if err != nil {
t.Fatalf("parse owner vector: %v", err)
}
addr.Arguments = rpc.Arguments{
{Name: rpc.RPC_COMMENT, DataType: rpc.DataString, Value: "hologram"},
}
integrated := addr.String()
if integrated == owner {
t.Skip("integrated rendering unavailable in this build")
}

if !sameINDEXAuthor(integrated, owner) {
t.Fatalf("integrated address %q not recognised as owner of %q", integrated, owner)
}
}
46 changes: 43 additions & 3 deletions tela_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,45 @@ func (a *App) InstallINDEX(indexJSON string) map[string]interface{} {
}
}

// sameINDEXAuthor reports whether the author stored in a TELA INDEX and an address
// rendered by walletapi belong to the same key.
//
// The two strings come from two different renderers, and on the simulator they
// disagree. The INDEX stores whatever the contract's address() returned, which is
// the DVM's ADDRESS_STRING, which builds the string with rpc.NewAddressFromKeys --
// and that leaves Mainnet true, always. It has to: consensus cannot depend on which
// network a node believes it is running. So a contract records dero1... on every
// chain, the simulator included. walletapi renders the same key through the wallet's
// own network flag and says deto1... there.
//
// A DERO address is bech32: the prefix names the network and the checksum is
// computed over that prefix, so both halves of the string differ and a string
// compare reads one wallet as two. The contract's own owner gate gets this right by
// accident -- it compares address() to address() -- so only the client-side check
// breaks, and only on the simulator. Comparing the decoded keys is what that check
// means.
func sameINDEXAuthor(stored, walletAddr string) bool {
if stored == "" || walletAddr == "" {
return false
}
if stored == walletAddr {
return true
}

a, err := rpc.NewAddress(stored)
if err != nil {
return false
}
b, err := rpc.NewAddress(walletAddr)
if err != nil {
return false
}

// Compare the compressed public keys. This drops the prefix, the checksum and
// any integrated payload -- none of which say anything about ownership.
return string(a.Compressed()) == string(b.Compressed())
}

// UpdateINDEX updates an existing TELA INDEX
func (a *App) UpdateINDEX(scid, indexJSON string) map[string]interface{} {
isSimulator := a.IsInSimulatorMode()
Expand Down Expand Up @@ -710,9 +749,10 @@ func (a *App) UpdateINDEX(scid, indexJSON string) map[string]interface{} {
}
}

// Check if wallet is owner
// Check if wallet is owner. Compare keys, not renderings -- see sameINDEXAuthor.
walletAddr := wallet.GetAddress().String()
if existingIndex.Author != walletAddr {
if !sameINDEXAuthor(existingIndex.Author, walletAddr) {
a.logToConsole(fmt.Sprintf("[ERR] INDEX owner mismatch: stored=%s wallet=%s", existingIndex.Author, walletAddr))
return map[string]interface{}{
"success": false,
"error": "Your wallet is not the owner of this INDEX. Only the original author can update it.",
Expand Down Expand Up @@ -809,7 +849,7 @@ func (a *App) GetINDEXInfo(scid string) map[string]interface{} {
wallet := GetWallet()
if wallet != nil {
walletAddr := wallet.GetAddress().String()
isOwner = index.Author == walletAddr
isOwner = sameINDEXAuthor(index.Author, walletAddr)
}

// "anon" author means immutable (ring 16+)
Expand Down
Loading