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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
77 changes: 77 additions & 0 deletions blockchain/k0_b2_consensus_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// K0 Fix B2 consensus-rule tests (spec k0-fix-design.md):
// SC_INSTALL at ringsize 2 rejected when the contract never calls SIGNER().
// ⚠️ DRAFT — research tooling, NOT part of DERO release code.
package blockchain

import (
"strings"
"testing"

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

// makeTestSCArguments builds the SCDATA argument set for an SC_INSTALL tx.
func makeTestSCArguments(code string) rpc.Arguments {
return rpc.Arguments{
rpc.Argument{Name: rpc.SCACTION, DataType: rpc.DataUint64, Value: uint64(rpc.SC_INSTALL)},
rpc.Argument{Name: rpc.SCCODE, DataType: rpc.DataString, Value: code},
}
}

func TestK0SCInstallRing2Reject_NoSignerRejected(t *testing.T) {
// contract never calls SIGNER() -> ringsize-2 install rejected
code := `
Function Initialize() Uint64
10 STORE("owner", "alice")
20 RETURN 0
End Function`
tx := &transaction.Transaction{}
tx.TransactionType = transaction.SC_TX
tx.SCDATA = makeTestSCArguments(code)
if err := k0SCInstallRing2Reject(tx); err == nil {
t.Fatal("no-SIGNER contract install at ringsize 2 should be rejected")
} else if !strings.Contains(err.Error(), "K0 Fix B2") && !strings.Contains(err.Error(), "K0 B2") {
t.Fatalf("wrong error: %v", err)
}
}

func TestK0SCInstallRing2Reject_SignerAllowed(t *testing.T) {
// contract calls SIGNER() -> ringsize-2 install allowed (owner-gated)
code := `
Function Initialize() Uint64
10 STORE("owner", SIGNER())
20 RETURN 0
End Function`
tx := &transaction.Transaction{}
tx.TransactionType = transaction.SC_TX
tx.SCDATA = makeTestSCArguments(code)
if err := k0SCInstallRing2Reject(tx); err != nil {
t.Fatalf("SIGNER contract install at ringsize 2 should be allowed: %v", err)
}
}

func TestK0SCInstallRing2Reject_NoSCDATAAllowed(t *testing.T) {
// SC_TX with no SCDATA isn't a real install; the no-op path handles it.
tx := &transaction.Transaction{}
tx.TransactionType = transaction.SC_TX
tx.SCDATA = rpc.Arguments{}
if err := k0SCInstallRing2Reject(tx); err != nil {
t.Fatalf("no-SCDATA SC_TX should not be rejected by B2 install rule: %v", err)
}
}

func TestK0SCInstallRing2Reject_CaseInsensitiveSigner(t *testing.T) {
// lowercase signer() must be detected (dispatch is case-insensitive)
code := `
Function Initialize() Uint64
10 STORE("owner", signer())
20 RETURN 0
End Function`
tx := &transaction.Transaction{}
tx.TransactionType = transaction.SC_TX
tx.SCDATA = makeTestSCArguments(code)
if err := k0SCInstallRing2Reject(tx); err != nil {
t.Fatalf("lowercase signer() contract should be allowed: %v", err)
}
}
22 changes: 20 additions & 2 deletions blockchain/transaction_execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,25 @@ func (chain *Blockchain) process_transaction_sc(cache map[crypto.Hash]*graviton.

meta := dvm.SC_META_DATA{}
if _, ok := sc.Functions["InitializePrivate"]; ok {
meta.Type = 1
meta.Type = dvm.SC_META_TYPE_PRIVATE
}
// K0 Fix B2: auto-detect SIGNER() usage. If the contract never
// calls SIGNER(), mark it NoSigner so ringsize-2 SC_TX calls to it
// are rejected (the ringsize-2 ring exposes the signer by design).
// Existing contracts (bit unset) default to uses_signer=true,
// preserving behavior.
//
// CHAIN-SPLIT HARDENING (wargame): this bit MUST only be set from the
// hard-fork height onward. It changes the SC_META tree bytes, which
// are committed into the chain state root (blockchain.go sc_change_cache
// -> tree hash). If a B2 node sets it before the fork while a legacy
// node doesn't, the same block produces DIFFERENT state roots on the
// two node types -> instant chain split. Gated on K0_MIN_RING4_HEIGHT
// (the same post-HF3 activation height as the B1 floor, PR #82 — the
// two K0 consensus rules turn on together); pre-fork installs write
// byte-identical meta to today.
if bl_height >= uint64(globals.Config.K0_MIN_RING4_HEIGHT) && !dvm.ContractUsesSigner(sc) {
meta.SetNoSigner(true)
}

w_sc_data_tree = dvm.Wrapped_tree(cache, ss, scid)
Expand All @@ -340,7 +358,7 @@ func (chain *Blockchain) process_transaction_sc(cache map[crypto.Hash]*graviton.
w_sc_tree.Put(dvm.SC_Meta_Key(scid), meta.MarshalBinary())

entrypoint := "Initialize"
if meta.Type == 1 { // if its a a private SC
if meta.IsPrivate() { // if its a a private SC (masks the B2 NoSigner bit)
entrypoint = "InitializePrivate"
}

Expand Down
57 changes: 57 additions & 0 deletions blockchain/transaction_verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

"github.com/deroproject/derohe/block"
"github.com/deroproject/derohe/config"
"github.com/deroproject/derohe/dvm"
"github.com/deroproject/derohe/cryptography/bn256"
"github.com/deroproject/derohe/cryptography/crypto"
"github.com/deroproject/derohe/globals"
Expand Down Expand Up @@ -205,6 +206,25 @@ func (chain *Blockchain) Expand_Transaction_NonCoinbase(tx *transaction.Transact
return chain.verify_Transaction_NonCoinbase_internal(true, tx)
}

// k0SCInstallRing2Reject implements the K0 Fix B2 rule for SC_INSTALL at
// ringsize 2: if the contract being installed never calls SIGNER(), the
// install itself has no legitimate reason to expose the signer at ringsize
// 2 — reject it. A contract that genuinely uses SIGNER() (owner-gated
// entrypoints) still installs at ringsize 2 until the verify_sig migration
// (K0 Fix C) removes that need.
func k0SCInstallRing2Reject(tx *transaction.Transaction) error {
if code, ok := tx.SCDATA.Value(rpc.SCCODE, rpc.DataString).(string); ok && code != "" {
sc, _, err := dvm.ParseSmartContract(code)
if err != nil {
return fmt.Errorf("K0 B2: could not parse SC code for SIGNER() scan: %v", err)
}
if !dvm.ContractUsesSigner(sc) {
return fmt.Errorf("K0 Fix B2: ringsize-2 SC_INSTALL rejected — contract never calls SIGNER() (privacy floor; use ringsize >= 4)")
}
}
return nil
}

// all non miner tx must be non-coinbase tx
// each check is placed in a separate block of code, to avoid ambigous code or faulty checks
// all check are placed and not within individual functions ( so as we cannot skip a check )
Expand Down Expand Up @@ -305,6 +325,43 @@ func (chain *Blockchain) verify_Transaction_NonCoinbase_internal(skip_proof bool
return fmt.Errorf("RingSize for %d statement cannot be more than 128.Actual %d", t, tx.Payloads[t].Statement.RingSize)
}

// K0 Fix B2 (k0-fix-design.md): a ringsize-2 SC_TX exposes the
// signer by design (the ring IS sender+receiver, parity selects the
// sender). If the invoked contract declares it does NOT use
// SIGNER() (NoSigner bit set at install via AST scan), a ringsize-2
// call is rejected — the caller should use ringsize >= 4. Contracts
// that genuinely call SIGNER() keep ringsize 2 (their owner-gated
// entrypoints require it until the verify_sig migration, Fix C).
//
// SC_INSTALL is checked separately from the code in SCDATA: an
// install at ringsize 2 whose contract never calls SIGNER() is also
// rejected (it exposes the deployer for no legitimate reason).
// CHAIN-SPLIT HARDENING (wargame): this rejection must ALSO be gated
// on the fork height — a B2 node rejecting a ring-2 no-SIGNER install
// pre-fork while a legacy node accepts it would make the two node
// types disagree on block validity -> split.
if tx.TransactionType == transaction.SC_TX && tx.Payloads[t].Statement.RingSize == 2 {
if action, ok := tx.SCDATA.Value(rpc.SCACTION, rpc.DataUint64).(uint64); ok && rpc.SC_ACTION(action) == rpc.SC_INSTALL && uint64(chain.Get_Height()) >= uint64(globals.Config.K0_MIN_RING4_HEIGHT) {
if err := k0SCInstallRing2Reject(tx); err != nil {
return err
}
}
if !tx.Payloads[t].SCID.IsZero() {
if version, verr := chain.ReadBlockSnapshotVersion(tx.BLID); verr == nil {
if ss_b2, serr := chain.Store.Balance_store.LoadSnapshot(version); serr == nil {
if sc_meta_tree, terr := ss_b2.GetTree(config.SC_META); terr == nil {
if meta_bytes, merr := sc_meta_tree.Get(dvm.SC_Meta_Key(tx.Payloads[t].SCID)); merr == nil && len(meta_bytes) >= 1 {
var meta dvm.SC_META_DATA
if meta.UnmarshalBinary(meta_bytes) == nil && meta.NoSigner() {
return fmt.Errorf("K0 Fix B2: ringsize-2 SC_TX rejected — contract %s declares no SIGNER() usage; use ringsize >= 4", tx.Payloads[t].SCID)
}
}
}
}
}
}
}

if !crypto.IsPowerOf2(len(tx.Payloads[t].Statement.Publickeylist_pointers) / int(tx.Payloads[t].Statement.Bytes_per_publickey)) {
return fmt.Errorf("corrupted key pointers")
}
Expand Down
6 changes: 4 additions & 2 deletions cmd/dero-wallet-cli/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -834,7 +834,9 @@ func ReadStringXSWDPrompt(l *readline.Instance, onClose chan bool, prompt string
prompt_mutex.Unlock()
}()

l.Operation.KickReader()
// KickReader() removed: no published readline implements it
// (wallet-only UI read-unblock helper, not consensus)
_ = l.Operation

input := make(chan string)
validValue := false
Expand All @@ -857,7 +859,7 @@ func ReadStringXSWDPrompt(l *readline.Instance, onClose chan bool, prompt string

select {
case <-onClose:
l.Operation.KickReader()
_ = l.Operation // KickReader() removed (UI shim, not consensus)
return ""
case a = <-input:
}
Expand Down
8 changes: 8 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ type CHAIN_CONFIG struct {
HF2_HEIGHT int64 // second HF applie here
MAJOR_HF2_HEIGHT int64 // MAJOR HF2 applies here, changes pow
MAJOR_HF3_HEIGHT int64 // MAJOR HF3 applied here, changes/adds consensus rules
// K0_MIN_RING4_HEIGHT: from this height onward, NORMAL/BURN txs with
// ringsize < 4 are rejected (PR #82, K0 Fix B1) AND ringsize-2 SC_TX
// requires the contract to declare SIGNER() usage (this PR, K0 Fix B2).
// Single activation height shared by B1 + B2 so the SC rules turn on
// with the floor.
K0_MIN_RING4_HEIGHT int64

Dev_Address string // to which address the integrator rewatd will go, if user doesn't specify integrator address'
Genesis_Tx string
Expand All @@ -108,6 +114,7 @@ var Mainnet = CHAIN_CONFIG{Name: "mainnet",
HF2_HEIGHT: 29000,
MAJOR_HF2_HEIGHT: 481600,
MAJOR_HF3_HEIGHT: 7504640,
K0_MIN_RING4_HEIGHT: 7600000, // post-HF3, mainnet (shared B1+B2 activation)

Genesis_Tx: "" +
"01" + // version
Expand All @@ -130,6 +137,7 @@ var Testnet = CHAIN_CONFIG{Name: "testnet", // testnet will always have last 3 b
HF2_HEIGHT: 0, // on testnet apply at genesis
MAJOR_HF2_HEIGHT: 4, // on testnet apply at 4
MAJOR_HF3_HEIGHT: 0, // on testnet apply at genesis
K0_MIN_RING4_HEIGHT: 0, // on testnet apply at genesis (immediate, shared B1+B2)

Genesis_Tx: "" +
"01" + // version
Expand Down
153 changes: 153 additions & 0 deletions dvm/k0_b2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// K0 Fix B2 tests (spec k0-fix-design.md, dvm-basic-improvements.md):
// NoSigner meta bit + SIGNER() auto-detection + 33-byte wire preservation.
// ⚠️ DRAFT — research tooling, NOT part of DERO release code.
package dvm

import (
"bytes"
"testing"
)

// --- ContractUsesSigner detection ---

func TestContractUsesSigner_DetectsCall(t *testing.T) {
code := `
Function Initialize() Uint64
10 STORE("owner", SIGNER())
20 RETURN 0
End Function
Function TransferOwnership() Uint64
30 IF SIGNER() != LOAD("owner") THEN GOTO 900
40 RETURN 0
900 RETURN 1
End Function`
sc, _, err := ParseSmartContract(code)
if err != nil {
t.Fatalf("parse: %v", err)
}
if !ContractUsesSigner(sc) {
t.Fatal("SIGNER() present but not detected")
}
}

func TestContractUsesSigner_NoFalsePositive(t *testing.T) {
code := `
Function Initialize() Uint64
10 STORE("owner", "alice")
20 RETURN 0
End Function
Function OwnerAction() Uint64
30 IF LOAD("owner") == "alice" THEN GOTO 40
35 RETURN 1
40 RETURN 0
End Function`
sc, _, err := ParseSmartContract(code)
if err != nil {
t.Fatalf("parse: %v", err)
}
if ContractUsesSigner(sc) {
t.Fatal("no SIGNER() call but detected as using signer")
}
}

func TestContractUsesSigner_CaseInsensitive(t *testing.T) {
// DVM dispatch lowercases function names; SIGNER/signer/Signer all resolve.
for _, s := range []string{"SIGNER()", "signer()", "Signer()"} {
code := "Function F() Uint64\n10 dim x as String\n20 LET x = " + s + "\n30 RETURN 0\nEnd Function"
sc, _, err := ParseSmartContract(code)
if err != nil {
t.Fatalf("parse %s: %v", s, err)
}
if !ContractUsesSigner(sc) {
t.Fatalf("%s not detected", s)
}
}
}

// --- SC_META_DATA NoSigner bit ---

func TestSCMetaNoSignerBit(t *testing.T) {
meta := SC_META_DATA{}
if meta.NoSigner() {
t.Fatal("fresh meta should not have NoSigner")
}
meta.SetNoSigner(true)
if !meta.NoSigner() {
t.Fatal("SetNoSigner did not stick")
}
if meta.IsPrivate() {
t.Fatal("NoSigner bit must not imply private")
}
// 33-byte wire format preserved.
if got := meta.MarshalBinary(); len(got) != 33 {
t.Fatalf("wire length = %d, want 33", len(got))
}
if got := meta.MarshalBinaryGood(); len(got) != 33 {
t.Fatalf("wire length (good) = %d, want 33", len(got))
}
}

func TestSCMetaPrivateAndNoSignerCoexist(t *testing.T) {
meta := SC_META_DATA{Type: SC_META_TYPE_PRIVATE}
meta.SetNoSigner(true)
if !meta.IsPrivate() {
t.Fatal("private flag lost when NoSigner set")
}
if !meta.NoSigner() {
t.Fatal("NoSigner lost when private set")
}
// Round-trip both serializations.
var m2 SC_META_DATA
if err := m2.UnmarshalBinary(meta.MarshalBinary()); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if !m2.IsPrivate() || !m2.NoSigner() {
t.Fatal("round-trip lost flags")
}
var m3 SC_META_DATA
if err := m3.UnmarshalBinaryGood(meta.MarshalBinaryGood()); err != nil {
t.Fatalf("unmarshal good: %v", err)
}
if !m3.IsPrivate() || !m3.NoSigner() {
t.Fatal("round-trip (good) lost flags")
}
}

func TestSCMeta33ByteCompatibility(t *testing.T) {
// A pre-B2 meta (Type=0 or 1, 33 bytes) unmarshals with NoSigner=false —
// existing on-chain metadata stays valid, existing contracts keep
// uses_signer=true behavior.
legacy := []byte{0}
legacy = append(legacy, make([]byte, 32)...)
var m SC_META_DATA
if err := m.UnmarshalBinary(legacy); err != nil {
t.Fatalf("legacy unmarshal: %v", err)
}
if m.NoSigner() {
t.Fatal("legacy meta must default to uses_signer (NoSigner unset)")
}
if m.IsPrivate() {
t.Fatal("Type=0 legacy must be open")
}

legacyPrivate := append([]byte{1}, make([]byte, 32)...)
var mp SC_META_DATA
if err := mp.UnmarshalBinary(legacyPrivate); err != nil {
t.Fatalf("legacy private unmarshal: %v", err)
}
if !mp.IsPrivate() {
t.Fatal("legacy private lost")
}
if mp.NoSigner() {
t.Fatal("legacy private must default uses_signer")
}
}

func TestSCMetaMarshalDeterminism(t *testing.T) {
meta := SC_META_DATA{Type: SC_META_TYPE_PRIVATE | SC_META_NOSIGNER_BIT}
b1 := meta.MarshalBinary()
b2 := meta.MarshalBinary()
if !bytes.Equal(b1, b2) {
t.Fatal("MarshalBinary not deterministic")
}
}
Loading