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
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,53 @@
Privacy-preserving token transfer application for the Horizen Vela platform. Includes a WASM runtime for confidential transactions inside the TEE and a client-side wallet CLI.

[runtime/](runtime/) <br/>
Application runtime for Horizen Vela
Application runtime for Horizen Vela

[wallet/](wallet) <br/>
Client-side wallet CLI

## Deanonymization Reports

The application supports deanonymization reports for regulatory compliance. An authorized auditor can request a report that is generated inside the TEE, encrypted, and stored in the Authority Service.

### Report Types

#### Balances Snapshot (default)

Returns all addresses with their current balance.

```bash
novaw requestreport
```

Report output:
```json
{
"accounts": {
"0xabc...": { "address": "0xabc...", "balance": "0x1bc16d674ec80000" }
},
"nonce": 5
}
```

#### Transaction History

Given a specific address, returns all transactions involving it (as sender or receiver): deposits, transfers, and withdrawals.

```bash
novaw requestreport --report-type tx_history --address 0xabc...
```

Report output:
```json
{
"address": "0xabc...",
"transactions": [
{ "type": "deposit", "from": "0xabc...", "to": "0xabc...", "amount": "0x...", "nonce": 1 },
{ "type": "transfer", "from": "0xabc...", "to": "0xdef...", "amount": "0x...", "nonce": 2, "invoice_id": "INV-001" },
{ "type": "withdrawal", "from": "0xabc...", "to": "0x123...", "amount": "0x...", "nonce": 3 }
]
}
```

Transaction history is stored in the private state and grows with each operation. Every deposit, transfer, and withdrawal appends a record that is then queryable via this report type.
89 changes: 78 additions & 11 deletions runtime/wasm-go/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,24 @@ import (

// --- High-Level Application Logic ---

// recordTransaction appends a transaction record to the state's transaction log.
// Must be called after state.Nonce++ so the nonce matches the corresponding event.
func recordTransaction(state *ApplicationInternalState, txType string, from, to types.Address, amount *types.Uint256, invoiceID string) {
amountCopy := *amount
state.Transactions = append(state.Transactions, TransactionRecord{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

state.Transactions will grow indefinetely and it will be a problem to manage a state too big. We need or to put a cap on the number of transactions saved and discard the oldest ones or we need to implement in Vela a way to manage big states. @paolocappelletti what do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would postpone to next impromevements.
Long term goal: the state should not be passed entirely to the wasm, but only accessed "on-request": the wasm interface should provide more low level primitives. es: a key-value access with methods get(key) set(key,value). then there will be a protocol for handling the call: executor -> encryption/decryption of the value -> manager -> db (commit only upon success of the request).
(This will allow also to charge fees for storage access/update as solidity does)
Will add some tickets in the backlog!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would add anyway a simple check that prevents that the list of transactions becomes too big, in the meantime we develop a better solution. Just to be on the safer side...

Copy link
Copy Markdown
Contributor

@paolocappelletti paolocappelletti Mar 10, 2026

Choose a reason for hiding this comment

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

ok seems reasonable.
let's add a max size constant @andreanistico (50?) and keep always the more recents

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I added a MaxTransactions constant and trim the state to that length, keeping the most recent

Type: txType,
From: from,
To: to,
Amount: &amountCopy,
Nonce: state.Nonce,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It would be useful to have a timestamp or a way to retrieve a timestamp from the Nonce

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

i added a timestamp

Timestamp: Now(),
InvoiceID: invoiceID,
})
if len(state.Transactions) > MaxTransactions {
state.Transactions = state.Transactions[len(state.Transactions)-MaxTransactions:]
}
}

func LoadModule(appId int64) types.LoadModuleResult {
initialState := &ApplicationInternalState{
AppID: uint64(appId),
Expand Down Expand Up @@ -74,6 +92,7 @@ func DepositFunds(senderPtr *types.Address, value *types.Uint256, stateJSON stri
return types.DepositResult{Error: fmt.Sprintf("Overflow while adding amount %s to balance: %s", value, oldBalance)}
}
currentState.Nonce++
recordTransaction(&currentState, "deposit", *senderPtr, *senderPtr, value, "")

// Create deposit event
eventData := DepositEvent{
Expand Down Expand Up @@ -204,6 +223,7 @@ func ProcessRequest(senderPtr *types.Address, requestType int32, payloadJSON, st
instructions.Transfer.Amount, oldRecipientBalance)}
}
currentState.Nonce++
recordTransaction(&currentState, "transfer", sender, instructions.Transfer.To, instructions.Transfer.Amount, instructions.Transfer.InvoiceID)

// Create events for both parties
senderEventData := SenderEvent{
Expand Down Expand Up @@ -270,6 +290,7 @@ func ProcessRequest(senderPtr *types.Address, requestType int32, payloadJSON, st
// Execute withdrawal
currentState.Accounts[senderHex].Balance.Sub(*currentState.Accounts[senderHex].Balance, *instructions.Withdraw.Amount)
currentState.Nonce++
recordTransaction(&currentState, "withdrawal", sender, instructions.Withdraw.To, instructions.Withdraw.Amount, "")

// Create withdrawal
withdrawals = append(withdrawals, types.Withdrawal{
Expand Down Expand Up @@ -298,23 +319,69 @@ func ProcessRequest(senderPtr *types.Address, requestType int32, payloadJSON, st
})

case "deanonymize":
// Generate deanonymization report
report := DeanonymizationReport{
Accounts: currentState.Accounts,
Nonce: currentState.Nonce,
reportType := "balances"
if instructions.Deanonymize != nil && instructions.Deanonymize.ReportType != "" {
reportType = instructions.Deanonymize.ReportType
}

var reportBytes []byte
var err error

switch reportType {
case "balances":
reportBytes, err = json.Marshal(DeanonymizationReport{
Accounts: currentState.Accounts,
Nonce: currentState.Nonce,
})
case "tx_history":
if instructions.Deanonymize.Address.IsZero() {
return types.ProcessResult{Error: "tx_history report requires a non-zero address"}
}
addr := instructions.Deanonymize.Address
addrHex := addr.Hex()
fromTs := instructions.Deanonymize.FromTimestamp
toTs := instructions.Deanonymize.ToTimestamp

filtered := []TransactionRecord{}
for _, tx := range currentState.Transactions {
if tx.From != addr && tx.To != addr {
continue
}
if fromTs > 0 && tx.Timestamp < fromTs {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Considering the way currentState.Transactions is implemented, the transactions should be ordered by timestamp, so the loop could be stopped the moment the transactions timestamps are outside the requested range

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed

continue
}
if toTs > 0 && tx.Timestamp > toTs {
break
}
filtered = append(filtered, tx)
}

// Look up current balance for the requested address
var balance *types.Uint256
if acc := currentState.Accounts[addrHex]; acc != nil {
balance = acc.Balance
} else {
balance = types.NewUint256(0)
}

reportBytes, err = json.Marshal(TxHistoryReport{
Address: addr,
Balance: balance,
Transactions: filtered,
})
default:
return types.ProcessResult{Error: fmt.Sprintf("Unsupported report type: %s", reportType)}
}

// Serialize the report
reportBytes, err := json.Marshal(report)
if err != nil {
utils.LogError("ProcessRequest: failed to serialize deanonymization report: %v", err)
return types.ProcessResult{Error: fmt.Sprintf("Failed to serialize deanonymization report: %v", err)}
utils.LogError("ProcessRequest: failed to serialize %s report: %v", reportType, err)
return types.ProcessResult{Error: fmt.Sprintf("Failed to serialize %s report: %v", reportType, err)}
}

utils.LogDebug("ProcessRequest: deanonymize sender=%s, accountsCount=%d, reportSize=%d",
senderHex, len(currentState.Accounts), len(reportBytes))
utils.LogDebug("ProcessRequest: deanonymize sender=%s, reportType=%s, reportSize=%d",
senderHex, reportType, len(reportBytes))
return types.ProcessResult{
State: []byte(stateJSON), //we have not modified the app state, using the old one to avoid useless marshalling
State: []byte(stateJSON),
Report: reportBytes,
Fuel: types.NewUint256(20),
}
Expand Down
37 changes: 30 additions & 7 deletions runtime/wasm-go/app/types.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package app

import (
"time"

"github.com/HorizenOfficial/vela-common-go/wasm/types"
)

// Now returns the current Unix timestamp. Defined as a variable so tests can override it.
var Now = func() int64 { return time.Now().Unix() }

const MaxInvoiceIDLength = 100
const MaxTransactions = 50

// ----- module internal types

Expand All @@ -14,11 +20,23 @@ type AccountState struct {
Balance *types.Uint256 `json:"balance"`
}

// TransactionRecord represents a single transaction stored in the private state
type TransactionRecord struct {
Type string `json:"type"` // "deposit", "transfer", "withdrawal"
From types.Address `json:"from"`
To types.Address `json:"to"`
Amount *types.Uint256 `json:"amount"`
Nonce uint64 `json:"nonce"`
Timestamp int64 `json:"timestamp"`
InvoiceID string `json:"invoice_id,omitempty"`
}

// ApplicationInternalState represents the internal state of the application
type ApplicationInternalState struct {
AppID uint64 `json:"appId"`
Accounts map[string]*AccountState `json:"accounts"`
Nonce uint64 `json:"nonce"`
AppID uint64 `json:"appId"`
Accounts map[string]*AccountState `json:"accounts"`
Nonce uint64 `json:"nonce"`
Transactions []TransactionRecord `json:"transactions,omitempty"`
}

// WithdrawInstruction represents instructions for withdrawing funds
Expand All @@ -37,6 +55,10 @@ type TransferInstruction struct {

// DeanonymizeInstruction represents optional instructions for deanonymization
type DeanonymizeInstruction struct {
ReportType string `json:"report_type,omitempty"` // "balances" (default) or "tx_history"
Address types.Address `json:"address,omitempty"` // required for tx_history
FromTimestamp int64 `json:"from_timestamp,omitempty"` // filter tx_history: start unix timestamp (inclusive)
ToTimestamp int64 `json:"to_timestamp,omitempty"` // filter tx_history: end unix timestamp (inclusive)
}

// PayloadInstructions represents the deserialized payload instructions
Expand All @@ -53,10 +75,11 @@ type DeanonymizationReport struct {
Nonce uint64 `json:"nonce"`
}

// ReportPayloadInstructions represents a specific information on how to generate a report
// TODO - We can add the list of the accounts to be included in the report and a boolean specifying whether
// we can omit empty accounts
type ReportPayloadInstructions struct {
// TxHistoryReport is the report returned for a tx_history deanonymization request
type TxHistoryReport struct {
Address types.Address `json:"address"`
Balance *types.Uint256 `json:"balance"`
Transactions []TransactionRecord `json:"transactions"`
}

type DepositEvent struct {
Expand Down
Loading
Loading