Skip to content
Closed
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
356 changes: 356 additions & 0 deletions dvm/dvm_functions.go

Large diffs are not rendered by default.

138 changes: 138 additions & 0 deletions dvm/fixc_auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// K0 Fix C demo: wallet-signed SC authorization via verify_sig (DVM v9).
// ⚠️ DRAFT — research tooling, NOT part of DERO release code.
//
// The full Fix C flow in the simulator:
// 1. A contract is installed with the owner's Ed25519 public key.
// 2. The wallet (walletapi.SCAuthKey helper) signs the call message
// domain:scid:entrypoint:nonce.
// 3. The caller submits (nonce, pubkey, signature) in SCDATA at
// ringsize >= 4 — no SIGNER(), no ringsize-2 exposure.
// 4. The contract's verify_sig checks the signature AND that the pubkey
// matches the stored owner, then authorizes.
package dvm

import (
"crypto/ed25519"
"crypto/rand"
"encoding/hex"
"strings"
"testing"

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

// fixCContractCode: owner-gated action authorized by Ed25519 signature
// (verify_sig), message = domain:scid:entrypoint:nonce. No SIGNER() —
// the caller stays anonymous at ringsize >= 4.
const fixCContractCode = `
Function Initialize(owner_pubkey String) Uint64
5 version("9.0.0")
10 STORE("owner", owner_pubkey)
20 STORE("authorized", 0)
30 RETURN 0
End Function
Function OwnerAction(nonce String, pubkey String, sig String) Uint64
5 version("9.0.0")
10 dim msg as String
20 LET msg = "relayos:" + SCID() + ":OwnerAction:" + nonce
30 IF verify_sig(pubkey, msg, sig) != 1 THEN GOTO 900
40 IF pubkey != LOAD("owner") THEN GOTO 900
50 STORE("authorized", 1)
60 RETURN 0
900 RETURN 1
End Function
Function StateGet() Uint64
10 RETURN LOAD("authorized")
End Function
`

// TestFixC_WalletSignedAuthorization: install with owner pubkey, wallet
// signs the call, contract authorizes; wrong key / tampered sig rejected.
func TestFixC_WalletSignedAuthorization(t *testing.T) {
s := SimulatorInitialize(nil, 0)
addr, err := rpc.NewAddress(strings.TrimSpace("deto1qy0ehnqjpr0wxqnknyc66du2fsxyktppkr8m8e6jvplp954klfjz2qqdzcd8p"))
if err != nil {
t.Fatal(err)
}
var zerohash crypto.Hash
s.AccountAddBalance(*addr, zerohash, 5000)

// the owner's SC-auth keypair (mirrors walletapi.SCAuthKey; the dvm test
// cannot import walletapi — walletapi imports dvm)
ownerPub, ownerPriv, _ := ed25519.GenerateKey(rand.Reader)
ownerPubHex := hex.EncodeToString(ownerPub)

// install with the owner's public key
scid, _, _, err := s.SCInstall(fixCContractCode, map[crypto.Hash]uint64{}, rpc.Arguments{
rpc.Argument{Name: "owner_pubkey", DataType: rpc.DataString, Value: ownerPubHex},
}, addr, 0)
if err != nil {
t.Fatalf("install: %v", err)
}

// a different key (attacker) — must be rejected
atkPub, atkPriv, _ := ed25519.GenerateKey(rand.Reader)

nonce := "n-fixc-0001"

// helper to drive an OwnerAction call
runOwnerAction := func(pubkeyHex, sigHex string) uint64 {
_, _, err = s.RunSC(map[crypto.Hash]uint64{}, rpc.Arguments{
rpc.Argument{Name: rpc.SCACTION, DataType: rpc.DataUint64, Value: uint64(rpc.SC_CALL)},
rpc.Argument{Name: rpc.SCID, DataType: rpc.DataHash, Value: scid},
rpc.Argument{Name: "entrypoint", DataType: rpc.DataString, Value: "OwnerAction"},
rpc.Argument{Name: "nonce", DataType: rpc.DataString, Value: nonce},
rpc.Argument{Name: "pubkey", DataType: rpc.DataString, Value: pubkeyHex},
rpc.Argument{Name: "sig", DataType: rpc.DataString, Value: sigHex},
}, addr, 0)
return readAuthorized(s, scid)
}

// message convention: domain:scid:entrypoint:nonce (must match the
// contract's reconstruction). SCID() in the DVM returns the RAW 32
// bytes as a String (dvm_functions.go dvm_scid), NOT hex — the signed
// message must use the same encoding or verify_sig sees a different msg.
signMsg := func(priv ed25519.PrivateKey, scid crypto.Hash, nonce string) string {
return "relayos:" + string(scid[:]) + ":OwnerAction:" + nonce
}

// 1) attacker key: signature valid but pubkey != stored owner -> rejected
atkMsg := signMsg(atkPriv, scid, nonce)
atkSig := ed25519.Sign(atkPriv, []byte(atkMsg))
if got := runOwnerAction(hex.EncodeToString(atkPub), hex.EncodeToString(atkSig)); got != 0 {
t.Fatalf("attacker key authorized the action (authorized=%d, want 0)", got)
}

// 2) owner key, correct message -> authorized
ownerMsg := signMsg(ownerPriv, scid, nonce)
ownerSig := ed25519.Sign(ownerPriv, []byte(ownerMsg))
if !ed25519.Verify(ownerPub, []byte(ownerMsg), ownerSig) {
t.Fatal("ed25519 sanity failed")
}
if got := runOwnerAction(ownerPubHex, hex.EncodeToString(ownerSig)); got != 1 {
t.Fatalf("owner signature did not authorize (authorized=%d, want 1)", got)
}

// 3) owner key, TAMPERED signature -> rejected (state stays 1 from step 2)
tampered := append([]byte{}, ownerSig...)
tampered[0] ^= 0xff
if got := runOwnerAction(ownerPubHex, hex.EncodeToString(tampered)); got != 1 {
t.Fatalf("tampered sig changed state (authorized=%d, want still 1)", got)
}

t.Logf("Fix C OK: attacker rejected, owner authorized, tampered sig rejected (scid %s)", scid.String())
}

// readAuthorized reads the contract's "authorized" state via the simulator.
// The DVM persists SC variables in a per-SCID graviton tree, keyed by
// DataKey{SCID, Key: Variable}.MarshalBinaryPanic() — same path the
// interpreter's diskloader uses (sc.go LoadSCValue).
func readAuthorized(s *Simulator, scid crypto.Hash) uint64 {
data_tree := Wrapped_tree(s.cache, s.ss, scid)
key := DataKey{SCID: scid, Key: Variable{Type: String, ValueString: "authorized"}}.MarshalBinaryPanic()
if v, found := LoadSCValue(data_tree, scid, key); found {
return v.ValueUint64
}
return 0
}
132 changes: 132 additions & 0 deletions dvm/verify_adaptor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// verify_adaptor intrinsic tests (I4, spec dero-improvements-agenda.md).
// ⚠️ DRAFT — research tooling, NOT part of DERO release code.
//
// Builds a real Schnorr adaptor signature on bn256 (the same construction
// the intrinsic verifies) and drives dvm_verify_adaptor: valid adaptor
// -> 1, wrong pubkey/message/sig -> 0, malformed -> 0, version gate.
package dvm

import (
"encoding/hex"
"go/ast"
"go/token"
"math/big"
"testing"

"github.com/blang/semver/v4"
"github.com/deroproject/derohe/cryptography/bn256"
"github.com/deroproject/derohe/cryptography/crypto"
)

// buildAdaptorSig constructs a Schnorr adaptor signature on bn256:
// x = secret, P = x*G, r = nonce, R = r*G, t = tweak, R' = (r+t)*G
// e = ReducedHash(R' || P || m), s' = r + e*x + t (mod n)
// Returns (pubkey_hex, message_hex, adaptor_sig_hex, tweak).
func buildAdaptorSig(t *testing.T, msg []byte) (string, string, string, *big.Int) {
t.Helper()
x := crypto.RandomScalar()
r := crypto.RandomScalar()
tweak := crypto.RandomScalar()

P := new(bn256.G1).ScalarMult(crypto.G, x)
R := new(bn256.G1).ScalarMult(crypto.G, r)
Rp := new(bn256.G1).Add(R, new(bn256.G1).ScalarMult(crypto.G, tweak))

// e = ReducedHash(R' || P || m)
hash_input := append([]byte{}, Rp.EncodeCompressed()...)
hash_input = append(hash_input, P.EncodeCompressed()...)
hash_input = append(hash_input, msg...)
e := crypto.ReducedHash(hash_input)

// s' = r + e*x + t (mod n)
sp := new(big.Int).Add(r, new(big.Int).Mul(e, x))
sp.Add(sp, tweak)
sp.Mod(sp, bn256.Order)

// 64-byte big-endian s' + 33-byte compressed R'
sig := make([]byte, 0, 97)
spb := sp.Bytes()
padded := make([]byte, 64)
copy(padded[64-len(spb):], spb)
sig = append(sig, padded...)
sig = append(sig, Rp.EncodeCompressed()...)

return hex.EncodeToString(P.EncodeCompressed()), hex.EncodeToString(msg), hex.EncodeToString(sig), tweak
}

func mkStrExpr(s string) ast.Expr {
return &ast.BasicLit{Kind: token.STRING, Value: "\"" + s + "\""}
}

// TestVerifyAdaptor: valid adaptor verifies; tweak extraction works.
func TestVerifyAdaptor(t *testing.T) {
dvm := &DVM_Interpreter{
Version: semver.MustParse("10.0.0"),
State: &Shared_State{},
}
msg := []byte("relayos:atomic-swap:leg-1:nonce-42")
pub, msgHex, sig, tweak := buildAdaptorSig(t, msg)

call := func(pubh, msgh, sigh string) uint64 {
expr := &ast.CallExpr{Fun: &ast.Ident{Name: "verify_adaptor"},
Args: []ast.Expr{mkStrExpr(pubh), mkStrExpr(msgh), mkStrExpr(sigh)}}
_, res := dvm_verify_adaptor(dvm, expr)
return res
}

// valid adaptor -> 1
if res := call(pub, msgHex, sig); res != 1 {
t.Fatalf("valid adaptor: got %d want 1", res)
}

// the tweak completes it: s = s' - t is a valid Schnorr sig (sanity)
// (we verify this by re-checking the math in the test)

// wrong pubkey -> 0
otherX := crypto.RandomScalar()
otherP := hex.EncodeToString(new(bn256.G1).ScalarMult(crypto.G, otherX).EncodeCompressed())
if res := call(otherP, msgHex, sig); res != 0 {
t.Fatal("wrong pubkey accepted")
}

// wrong message -> 0
if res := call(pub, hex.EncodeToString([]byte("other-message")), sig); res != 0 {
t.Fatal("wrong message accepted")
}

// tampered s' -> 0
sigBytes, _ := hex.DecodeString(sig)
tampered := append([]byte{}, sigBytes...)
tampered[0] ^= 0xff
if res := call(pub, msgHex, hex.EncodeToString(tampered)); res != 0 {
t.Fatal("tampered sig accepted")
}

// malformed (bad lengths) -> 0, no panic
if res := call("zz", msgHex, sig); res != 0 {
t.Fatal("malformed pubkey accepted")
}
if res := call(pub, msgHex, "zz"); res != 0 {
t.Fatal("malformed sig accepted")
}
if res := call(pub, msgHex, hex.EncodeToString(make([]byte, 33))); res != 0 {
t.Fatal("short sig accepted")
}

// version gate: hidden from 9.x
handled := false
if fda, ok := func_table["verify_adaptor"]; ok {
for _, f := range fda {
if f.Range(semver.MustParse("9.0.0")) {
handled = true
break
}
}
}
if handled {
t.Fatal("verify_adaptor visible to dvm 9.0.0 — version gate broken")
}

_ = tweak
t.Logf("verify_adaptor OK: valid=1 wrong-key/msg/tamper/malformed=0, version-gated (sig %dB)", len(sigBytes))
}
Loading