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
16 changes: 15 additions & 1 deletion cmd/vsc-node/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,10 +240,24 @@ func main() {

sr := streamer.NewStreamReader(hiveBlocks, blockConsumer.ProcessBlock, se.SaveBlockHeight, stBlock)

flatDb, err := flatfs.CreateOrOpen(path.Join(args.dataDir, "tss-keys"), flatfs.Prefix(1), false)
tssKeysDir := path.Join(args.dataDir, "tss-keys")
flatDb, err := flatfs.CreateOrOpen(tssKeysDir, flatfs.Prefix(1), false)
if err != nil {
panic(err)
}
// M58-O6: the TSS keystore holds the node's threshold-signature share
// material (per key id / epoch). go-ds-flatfs creates this directory with
// os.MkdirAll(path, 0755) (flatfs.go) and writes shard files at 0666, both
// subject only to umask — so a misconfigured umask, an `cp` without -p, or a
// `tar --no-same-permissions` extract leaves the directory world-readable,
// letting any adjacent local user enumerate the TSS key ids and epochs.
// Mirror the owner-only treatment the identity config already gets (GV-H7 /
// review2 CRITICAL #5 in modules/config/config.go) by forcing the keystore
// directory to 0700 regardless of umask. flatfs created/opened it above, so
// the chmod is unconditional and idempotent.
if err := os.Chmod(tssKeysDir, 0700); err != nil {
panic(err)
}

tssMgr := tss.New(
p2p,
Expand Down
11 changes: 11 additions & 0 deletions lib/dids/eth.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,17 @@ func (d EthDID) Verify(block blocks.Block, sig string) (bool, error) {
return false, fmt.Errorf("failed to decode signature: %v", err)
}

// audit N6-H4: EthDID.Verify is a crypto primitive reached on a
// permissionless path (OffchainTransaction.Verify -> VerifySignatures ->
// VerifyMany). An attacker-supplied signature shorter than 65 bytes made the
// sigBytes[64] access below panic (index out of range). Reject malformed
// lengths up front like every other DID verifier in this package
// (btc.go:120/129, bls.go:167, gateway_pop.go:89). A valid secp256k1
// recoverable signature is always exactly 65 bytes, so no valid sig changes.
if len(sigBytes) != 65 {
return false, fmt.Errorf("invalid signature length for DID %s: got %d, want 65", d.String(), len(sigBytes))
}

if sigBytes[64] != 0 && sigBytes[64] != 1 {
sigBytes[64] -= 27
}
Expand Down
68 changes: 48 additions & 20 deletions modules/common/consensusversion/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,59 @@ import (
// - 0.1.0 — pendulum settlement rollout. The Consensus 0→1 bump is what lets the floor
// rise to exclude pre-pendulum (0.0.0) nodes from the committee and TSS once a
// vsc.propose_consensus_version activates (see docs/consensus-upgrades.md).
// - 0.2.0 — the Consensus 1→2 bump gates TWO independent consensus changes, both
// activated together when the election floor reaches 0.2.0:
// (a) try/catch inter-contract calls (ICCallOptions.Try): a caught revert returns a
// structured outcome + rolls back to a savepoint instead of trapping. Until the
// floor reaches 0.2.0 the Try flag is IGNORED and a reverting callee traps as
// before. See TryCatchICCVersion.
// (b) pendulum LP minimum-floor (B12): the swap-fee split caps the node fraction at
// BpsScale − MinFractionBps (including on the under-secured cliff), so liquidity
// providers always retain a minimum share of every pot. Gated on this line via
// pendulum.LPFloorActivation.
// Until 0.2.0 is chain-active both behaviors are inert and splits/call semantics stay
// byte-identical to 0.1.0, so old and new binaries interoperate until activation.
// - 0.2.0 — try/catch inter-contract calls (ICCallOptions.Try). The Consensus 1→2 bump
// gates the new contracts.call semantics (a caught revert returns a structured
// outcome + rolls back to a savepoint instead of trapping). Until the floor reaches
// 0.2.0 the Try flag is IGNORED and a reverting callee traps as before, so old and
// new binaries stay byte-identical pre-activation. See TryCatchICCVersion.
// - 0.3.0 — WASM _initialize hard-fail (MED #130 / m57 F-WB-3). Below this floor the
// host discards the result of the contract's `_initialize` export and dispatches the
// action handler anyway; on a TinyGo (Go runtime) contract whose `_initialize` traps,
// the module init-flag stays 0 and the handler silently no-ops (br_if over its body)
// yet the call reports success. The 2→3 bump makes the host ABORT the call with
// WASM_INIT_ERROR when `_initialize` traps, instead of running a half-initialised
// handler. See WasmInitGuardVersion.
const (
currentMajor uint64 = 0
currentConsensus uint64 = 2
currentConsensus uint64 = 3
currentNonConsensus uint64 = 0
)

// TryCatchICCVersion is the minimum chain-active consensus version at which the
// try/catch inter-contract-call semantics (ICCallOptions.Try) take effect. Below
// it a Try call behaves exactly like a legacy call (a reverting callee traps the
// caller), so activation is fully coordinated by the election version floor. It
// is part of the v0.2.0 batch, so it is the same line as V0_2_0 (see
// feature_gates.go) — kept as its own named var so the try/catch gate reads by
// FEATURE at its call site.
var TryCatchICCVersion = V0_2_0
// caller), so activation is fully coordinated by the election version floor.
var TryCatchICCVersion = Version{Major: 0, Consensus: 2, NonConsensus: 0}

// SdkErrorDeterminismVersion is the minimum chain-active consensus version at
// which the deterministic system.call panic-detail rendering takes effect. It
// ships in the v0.2.0 batch (Consensus 1->2), so it is keyed to the same {0,2,0}
// triple as TryCatchICCVersion, but is kept as its OWN named gate so the two
// v0.2.0-era features stay independently traceable (matching the per-feature
// gate convention in ConsensusParams).
//
// Below it, system.call renders a recovered panic value with
// fmt.Errorf("%v", r) (legacy) — which can leak per-node pointer addresses /
// goroutine ids and so fork the result CID. At/after it, the recovered panic
// value is rendered deterministically (errors/strings verbatim, any other type
// as its concrete type name only). Because that string flows into the contract
// result -> state diff -> CID, the switch is consensus-affecting and MUST be
// coordinated by the election version floor, exactly like TryCatchICCVersion.
//
// (The related sp1_verify_groth16 error-class fix — MED-37 — and the
// contracts.read presence-disambiguation fix — MED-119 — are NOT gated here:
// each ships as an additive sibling host function (sp1_verify_groth16_ex /
// contracts.read_ex) that changes no existing contract's behaviour and so needs
// no height gate.)
var SdkErrorDeterminismVersion = Version{Major: 0, Consensus: 2, NonConsensus: 0}

// WasmInitGuardVersion is the minimum chain-active consensus version at which the
// WASM host aborts a contract call whose `_initialize` export trapped (MED #130 /
// m57 F-WB-3). Below it the host preserves the legacy behaviour — the `_initialize`
// result is discarded and the action handler runs even with an uninitialised module
// (a silent no-op on TinyGo contracts) — so old and new binaries stay byte-identical
// pre-activation. Activation is coordinated solely by the election version floor.
var WasmInitGuardVersion = Version{Major: 0, Consensus: 3, NonConsensus: 0}

// ParseComponent parses a numeric version-component string, defaulting to 0 when
// empty/invalid. Retained for the announcement payload helper; the running version itself
Expand Down Expand Up @@ -79,8 +106,8 @@ func RunningVersion() Version {

// Version is the canonical on-chain / wire representation.
type Version struct {
Major uint64 `json:"major" bson:"version_major,omitempty"`
Consensus uint64 `json:"consensus" bson:"version_consensus,omitempty"`
Major uint64 `json:"major" bson:"version_major,omitempty"`
Consensus uint64 `json:"consensus" bson:"version_consensus,omitempty"`
NonConsensus uint64 `json:"non_consensus" bson:"version_non_consensus,omitempty"`
}

Expand Down Expand Up @@ -149,3 +176,4 @@ func MaxComponentwise(a, b Version) Version {
}
return out
}

2 changes: 1 addition & 1 deletion modules/common/consensusversion/version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func TestMaxComponentwise(t *testing.T) {
// pinned current triple. Update this pin in the SAME commit that bumps the constants in
// version.go.
func TestRunningVersionIsSourcePinned(t *testing.T) {
want := Version{Major: 0, Consensus: 2, NonConsensus: 0}
want := Version{Major: 0, Consensus: 3, NonConsensus: 0}
if got := RunningVersion(); got != want {
t.Fatalf("running version = %+v, want %+v (source constants in version.go)", got, want)
}
Expand Down
86 changes: 82 additions & 4 deletions modules/contract/execution-context/execution-context.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,21 @@ type contractExecutionContext struct {
// across binaries. Set once per tx by the state engine and propagated into
// nested calls.
tryCatchActive bool

// sdkErrorDeterminismActive gates the v0.2.0 deterministic SDK
// error-surfacing behaviour (>= SdkErrorDeterminismVersion). False => SDK
// host functions render errors exactly as the pre-fix code did (byte-
// identical legacy output). Set once per tx by the state engine from the
// chain-active consensus version and propagated into nested calls, so it is
// height-addressable and identical across nodes — mirrors tryCatchActive.
sdkErrorDeterminismActive bool

// initGuardActive gates the WASM host's `_initialize` hard-fail (MED #130 /
// m57 F-WB-3) on the chain-active consensus version (>= WasmInitGuardVersion).
// False => a trapping `_initialize` is ignored and the (uninitialised) handler
// runs as before, keeping pre-activation behaviour identical across binaries.
// Set once per tx by the state engine and propagated into nested calls.
initGuardActive bool
}

type ContractExecutionContext = *contractExecutionContext
Expand Down Expand Up @@ -163,6 +178,29 @@ func WithTryCatch(active bool) Option {
return func(ctx *contractExecutionContext) { ctx.tryCatchActive = active }
}

// WithSdkErrorDeterminism enables the v0.2.0 deterministic SDK error-surfacing
// behaviour. The state engine sets this from the chain-active consensus version
// (>= consensusversion.SdkErrorDeterminismVersion) so it only activates at a
// coordinated version floor; when false the SDK host functions emit byte-
// identical legacy output.
func WithSdkErrorDeterminism(active bool) Option {
return func(ctx *contractExecutionContext) { ctx.sdkErrorDeterminismActive = active }
}

// SdkErrorDeterminismActive implements wasm_context.ExecContextValue. It exposes
// the per-call, height-addressable gate to the SDK host functions.
func (ctx *contractExecutionContext) SdkErrorDeterminismActive() bool {
return ctx.sdkErrorDeterminismActive
}

// WithInitGuard enables the WASM host's `_initialize` hard-fail (MED #130). The
// state engine sets this from the chain-active consensus version so the abort-on-
// failed-init semantics only activate at a coordinated version floor. It is
// propagated into nested contracts.call contexts unchanged.
func WithInitGuard(active bool) Option {
return func(ctx *contractExecutionContext) { ctx.initGuardActive = active }
}

func (ctx *contractExecutionContext) IOGas() int {
return (ctx.ioReadGas*params.READ_IO_GAS_RC_COST + ctx.ioWriteGas*params.WRITE_IO_GAS_RC_COST) * params.CYCLE_GAS_PER_RC
}
Expand Down Expand Up @@ -574,6 +612,32 @@ func (ctx *contractExecutionContext) ContractStateGet(contractId string, key str
return result.Ok(resStr)
}

// ContractStateGetEx is the presence-disambiguating sibling of ContractStateGet
// (MED-119). ContractStateGet collapses both "key absent" and "key present with
// an empty value" to result.Ok("") — a cross-contract reader (e.g. the ZK
// verifier blocklist read of another contract's "b-{height}" key) cannot tell a
// stale/missing entry from an intentional empty one. This sibling returns the
// same value string PLUS an `exists` flag derived from the same nil-vs-non-nil
// distinction the state store already computes deterministically from the
// merkle-backed databin (StateStore.Get returns nil only when the key is truly
// absent; an empty stored value comes back as a non-nil empty slice). The
// existence bit is therefore height-deterministic and identical across nodes.
//
// Gas is charged byte-for-byte identically to ContractStateGet (same doIO calls
// in the same order) so the only observable difference for a contract that opts
// in via contracts.read_ex is the extra `exists` field — and existing contracts,
// which import only contracts.read, are completely unaffected.
func (ctx *contractExecutionContext) ContractStateGetEx(contractId string, key string) (result.Result[string], bool) {
ctx.doIO(len(key))
res := ctx.callSession.GetStateStore(contractId).Get(key)
if res == nil {
return result.Ok(""), false
}
resStr := string(res)
ctx.doIO(len(resStr))
return result.Ok(resStr), true
}

// tryOutcome encodes the structured result a try/catch inter-contract call hands
// back to the caller (instead of trapping). The caller's SDK decodes the "ok"
// field to branch. gas is what the callee actually consumed — charged either way,
Expand Down Expand Up @@ -653,15 +717,29 @@ func (ctx *contractExecutionContext) ContractCall(
// the GraphQL simulate path), this propagates nil — same
// behaviour as before, just now consistent across the call depth.
WithPendulumApplier(ctx.pendulumApplier),
WithTryCatch(ctx.tryCatchActive))
WithTryCatch(ctx.tryCatchActive),
// Propagate the v0.2.0 SDK error-determinism gate into the nested
// context so a contract reached via contracts.call evaluates it
// identically to the entrypoint (height-addressable, same on all
// nodes) — mirrors the tryCatchActive propagation above.
WithSdkErrorDeterminism(ctx.sdkErrorDeterminismActive),
WithInitGuard(ctx.initGuardActive))

callPayload := payload
json.Unmarshal([]byte(payloadJson), &callPayload)

wasmCtx := context.WithValue(
context.WithValue(context.Background(), wasm_context.WasmExecCtxKey, ctxValue),
wasm_context.WasmExecCodeCtxKey,
hex.EncodeToString(ct.Code),
context.WithValue(
context.WithValue(context.Background(), wasm_context.WasmExecCtxKey, ctxValue),
wasm_context.WasmExecCodeCtxKey,
hex.EncodeToString(ct.Code),
),
// MED #130 (m57 F-WB-3): propagate the chain-active `_initialize`
// hard-fail gate into the nested call so a callee with a trapping
// `_initialize` aborts (post-activation) instead of silently
// no-opping, matching the top-level entry semantics.
wasm_context.WasmInitGuardCtxKey,
ctx.initGuardActive,
)
// try/catch (ICCallOptions.Try): snapshot state + ledger just before
// the callee runs, so a revert can be unwound without disturbing the
Expand Down
Loading