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
83 changes: 52 additions & 31 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1764,47 +1764,28 @@ func startValidator(
// Phase 9 write-back is part of every proof cycle (RB3-F75): the principal, the signer and the
// submitter are required, and a validator that cannot build them does not start. There is no
// disabled mode - it used to run every cycle with its results written nowhere - no null submitter,
// and no fallback to the validator's key for a malformed write-back key.
// and no fallback to the validator's key: the write-back key is required (RB4-F50).
var accSubmitter execution.AccumulateSubmitter

accWritebackPrincipal := os.Getenv("ACCUMULATE_RESULTS_PRINCIPAL")
accSignerURL := os.Getenv("ACCUMULATE_SIGNER_URL")
if v := os.Getenv("FF_UNIFIED_TABLES"); v != "" && v != "true" {
return nil, nil, fmt.Errorf("FF_UNIFIED_TABLES=%s is not supported: every proof cycle stores its evidence", v)
}
if v := os.Getenv("PROOF_CYCLE_WRITEBACK"); v != "" && v != "true" {
return nil, nil, fmt.Errorf("PROOF_CYCLE_WRITEBACK=%s is not supported: proof cycles always write their results back", v)
}

wb, err := writebackSettingsFromEnv()
if err != nil {
return nil, nil, err
}
{
if accWritebackPrincipal == "" || accSignerURL == "" {
return nil, nil, fmt.Errorf("write-back requires ACCUMULATE_RESULTS_PRINCIPAL and ACCUMULATE_SIGNER_URL " +
"(write-back cannot run without them)")
}
log.Printf("📝 [Phase 9] Configuring Accumulate write-back:")
log.Printf(" - Principal: %s", accWritebackPrincipal)
log.Printf(" - Signer: %s", accSignerURL)

// An optional dedicated write-back key. If it is set it must be valid: a malformed key is a
// configuration error, not a reason to sign with the validator's own key instead.
writebackPrivKey := privateKey
if writebackKeyHex := os.Getenv("ACCUMULATE_WRITEBACK_PRIV_KEY"); writebackKeyHex != "" {
keyBytes, err := hex.DecodeString(strings.TrimSpace(writebackKeyHex))
if err != nil {
return nil, nil, fmt.Errorf("ACCUMULATE_WRITEBACK_PRIV_KEY is not valid hex: %w", err)
}
if len(keyBytes) != ed25519.PrivateKeySize {
return nil, nil, fmt.Errorf("ACCUMULATE_WRITEBACK_PRIV_KEY is %d bytes, want %d", len(keyBytes), ed25519.PrivateKeySize)
}
writebackPrivKey = ed25519.PrivateKey(keyBytes)
log.Printf(" - Using the dedicated write-back key from ACCUMULATE_WRITEBACK_PRIV_KEY")
}
log.Printf(" - Principal: %s", wb.principal)
log.Printf(" - Signer: %s", wb.signer)
log.Printf(" - Key: %x", wb.key.Public())

submitterCfg := &execution.AccumulateSubmitterConfig{
Client: liteClientAdapter,
PrivateKey: writebackPrivKey,
AccountURL: accWritebackPrincipal,
SignerURL: accSignerURL,
PrivateKey: wb.key,
AccountURL: wb.principal,
SignerURL: wb.signer,
KeyPageIndex: 1,
KeyIndex: 0,
ConfirmationTimeout: 2 * time.Minute,
Expand Down Expand Up @@ -1887,7 +1868,7 @@ func startValidator(
AttestationPeers: cfg.AttestationPeers,
AttestationRequiredCount: cfg.AttestationRequiredCount,
AccumulateClient: accSubmitter,
ResultsPrincipal: accWritebackPrincipal,
ResultsPrincipal: wb.principal,
Ed25519Key: privateKey,
EnableMultiChain: cfg.EnableMultiChain,
ProofGenerator: proofGenAdapter,
Expand Down Expand Up @@ -2192,6 +2173,45 @@ func bftTimeoutFromEnv() (time.Duration, error) {
}

// checkpointSettings is the block-checkpoint anchor's configuration, all of it.
type writebackSettings struct {
principal, signer string
key ed25519.PrivateKey
}

// writebackSettingsFromEnv reads Phase 9 write-back's settings, refusing any that is missing or malformed.
// The key is required: a validator without ACCUMULATE_WRITEBACK_PRIV_KEY used to sign its write-backs with
// its own consensus key, which is not on the signer's key page (RB4-F50).
func writebackSettingsFromEnv() (writebackSettings, error) {
wb := writebackSettings{
principal: strings.TrimSpace(os.Getenv("ACCUMULATE_RESULTS_PRINCIPAL")),
signer: strings.TrimSpace(os.Getenv("ACCUMULATE_SIGNER_URL")),
}
if v := os.Getenv("PROOF_CYCLE_WRITEBACK"); v != "" && v != "true" {
return wb, fmt.Errorf("PROOF_CYCLE_WRITEBACK=%s is not supported: proof cycles always write their results back", v)
}
keyHex := strings.TrimSpace(os.Getenv("ACCUMULATE_WRITEBACK_PRIV_KEY"))
var missing []string
for name, v := range map[string]string{
"ACCUMULATE_RESULTS_PRINCIPAL": wb.principal, "ACCUMULATE_SIGNER_URL": wb.signer,
"ACCUMULATE_WRITEBACK_PRIV_KEY": keyHex,
} {
if v == "" {
missing = append(missing, name)
}
}
if len(missing) > 0 {
sort.Strings(missing)
return wb, fmt.Errorf("proof cycles always write their results back, but %s is not set", strings.Join(missing, ", "))
}
kb, err := hex.DecodeString(keyHex)
if err != nil || len(kb) != ed25519.PrivateKeySize {
// The value is a secret: named, never printed.
return wb, fmt.Errorf("ACCUMULATE_WRITEBACK_PRIV_KEY is not a %d-byte hex ed25519 private key", ed25519.PrivateKeySize)
}
wb.key = ed25519.PrivateKey(kb)
return wb, nil
}

type checkpointSettings struct {
writer, account, signer string
key ed25519.PrivateKey
Expand Down Expand Up @@ -2249,6 +2269,7 @@ func checkEnvironment() error {
func() error { _, err := bftTimeoutFromEnv(); return err },
func() error { _, err := envvar.Bool("MIGRATE_ON_START", false); return err },
func() error { _, err := accumulate.LogLevelFromEnv(); return err },
func() error { _, err := writebackSettingsFromEnv(); return err },
func() error {
enabled, err := envvar.Bool("CHECKPOINT_ANCHOR_ENABLED", false)
if err != nil || !enabled {
Expand Down
35 changes: 35 additions & 0 deletions main_env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ func TestBootAcceptsProductionsValues(t *testing.T) {
for k, v := range map[string]string{
"BATCH_PERIOD_BLOCKS": "100", "CERTEN_ALLOW_CONTRACT_CALLS": "true", "CERTEN_BLOCK_RETENTION": "0",
"CERTEN_ENTITLEMENT_REFRESH_SEC": "10", "ON_DEMAND_INTENT_KEYED": "true", "CHECKPOINT_ANCHOR_ENABLED": "false",
"PROOF_CYCLE_WRITEBACK": "true", "ACCUMULATE_RESULTS_PRINCIPAL": "acc://certen-protocol.acme/proof-results",
"ACCUMULATE_SIGNER_URL": "acc://certen-protocol.acme/book/1", "ACCUMULATE_WRITEBACK_PRIV_KEY": hex.EncodeToString(make([]byte, 64)),
} {
t.Setenv(k, v)
}
Expand Down Expand Up @@ -87,3 +89,36 @@ func TestCheckpointAnchorIsConfiguredWholeOrRefused(t *testing.T) {
t.Fatalf("an enabled, unconfigured checkpoint anchor passed the boot check: %v", err)
}
}

// RB4-F50: without ACCUMULATE_WRITEBACK_PRIV_KEY a validator signed its write-backs with its own key.
func TestWritebackIsConfiguredWholeOrRefused(t *testing.T) {
key := hex.EncodeToString(make([]byte, 64))
set := func(principal, signer, wbKey string) {
t.Setenv("ACCUMULATE_RESULTS_PRINCIPAL", principal)
t.Setenv("ACCUMULATE_SIGNER_URL", signer)
t.Setenv("ACCUMULATE_WRITEBACK_PRIV_KEY", wbKey)
}
set("acc://x.acme/results", "acc://x.acme/book/1", key)
wb, err := writebackSettingsFromEnv()
if err != nil || wb.signer != "acc://x.acme/book/1" || len(wb.key) != 64 {
t.Fatalf("a whole configuration was refused: %+v %v", wb, err)
}
for name, args := range map[string][3]string{
"no principal": {"", "acc://x.acme/book/1", key},
"no signer": {"acc://x.acme/results", "", key},
"no write-back key": {"acc://x.acme/results", "acc://x.acme/book/1", ""},
"malformed key": {"acc://x.acme/results", "acc://x.acme/book/1", "zz"},
"short key": {"acc://x.acme/results", "acc://x.acme/book/1", key[:64]},
} {
set(args[0], args[1], args[2])
if _, err := writebackSettingsFromEnv(); err == nil {
t.Errorf("%s: accepted", name)
} else if strings.Contains(err.Error(), key[:64]) {
t.Errorf("%s: the refusal printed the key", name)
}
}
set("acc://x.acme/results", "acc://x.acme/book/1", "")
if err := checkEnvironment(); err == nil || !strings.Contains(err.Error(), "ACCUMULATE_WRITEBACK_PRIV_KEY") {
t.Fatalf("a validator without its write-back key passed the boot check: %v", err)
}
}
37 changes: 35 additions & 2 deletions main_startup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestValidatorRefusesToStartWithoutItsDatabase(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestValidatorRefusesToStartWithoutItsDatabase$")
cmd.Env = append(os.Environ(),
cmd.Env = append(append(os.Environ(), writebackEnv()...),
"CERTEN_STARTUP_UNDER_TEST=1",
"VALIDATOR_ID=validator-startup-test",
"DATABASE_URL=postgres://certen@127.0.0.1:1/none?sslmode=disable&connect_timeout=2",
Expand Down Expand Up @@ -59,7 +59,7 @@ func TestValidatorRefusesTheDatabaseOptionalSetting(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestValidatorRefusesTheDatabaseOptionalSetting$")
cmd.Env = append(os.Environ(),
cmd.Env = append(append(os.Environ(), writebackEnv()...),
"CERTEN_STARTUP_UNDER_TEST=2",
"VALIDATOR_ID=validator-startup-test",
"DATABASE_URL=postgres://certen@127.0.0.1:1/none?sslmode=disable&connect_timeout=2",
Expand All @@ -72,6 +72,39 @@ func TestValidatorRefusesTheDatabaseOptionalSetting(t *testing.T) {
}
}

// RB4-F50: a validator without ACCUMULATE_WRITEBACK_PRIV_KEY used to sign its write-backs with its own
// key. It refuses to start, naming the value.
func TestValidatorRefusesToStartWithoutItsWritebackKey(t *testing.T) {
if os.Getenv("CERTEN_STARTUP_UNDER_TEST") == "3" {
os.Args = []string{"validator"}
main()
return
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestValidatorRefusesToStartWithoutItsWritebackKey$")
cmd.Env = append(append(os.Environ(), writebackEnv()...),
"CERTEN_STARTUP_UNDER_TEST=3",
"VALIDATOR_ID=validator-startup-test",
"ACCUMULATE_WRITEBACK_PRIV_KEY=",
)
out, err := cmd.CombinedOutput()
log := string(out)
if ctx.Err() != nil || err == nil || !strings.Contains(log, "ACCUMULATE_WRITEBACK_PRIV_KEY is not set") {
t.Fatalf("a validator without its write-back key must refuse to start by name (err=%v):\n%s", err, tail(log))
}
}

// writebackEnv is a whole write-back configuration, so a startup test reaches the check it is about.
func writebackEnv() []string {
return []string{
"PROOF_CYCLE_WRITEBACK=true",
"ACCUMULATE_RESULTS_PRINCIPAL=acc://startup-test.acme/results",
"ACCUMULATE_SIGNER_URL=acc://startup-test.acme/book/1",
"ACCUMULATE_WRITEBACK_PRIV_KEY=" + strings.Repeat("00", 64),
}
}

func tail(s string) string {
lines := strings.Split(strings.TrimSpace(s), "\n")
if len(lines) > 25 {
Expand Down
3 changes: 2 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,8 @@ See [Proof Classes](#proof-classes) for what these control.

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `PROOF_CYCLE_WRITEBACK` | No | — | Only `true` is accepted; any other value is refused. Proof cycles always write their results back (requires `ACCUMULATE_RESULTS_PRINCIPAL`, `ACCUMULATE_SIGNER_URL`) |
| `PROOF_CYCLE_WRITEBACK` | No | — | Only `true` is accepted; any other value is refused. Proof cycles always write their results back (requires `ACCUMULATE_RESULTS_PRINCIPAL`, `ACCUMULATE_SIGNER_URL`, `ACCUMULATE_WRITEBACK_PRIV_KEY`) |
| `ACCUMULATE_WRITEBACK_PRIV_KEY` | Yes | — | Hex ed25519 private key (64 bytes) on `ACCUMULATE_SIGNER_URL`'s key page that signs write-backs. The validator refuses to start without it (it used to sign with its own key) |
| `FIRESTORE_ENABLED` | No | false | Enable Firestore real-time sync |
| `FIREBASE_PROJECT_ID` | No | - | Firebase project ID |
| `GOOGLE_APPLICATION_CREDENTIALS` | No | - | Service account JSON path |
Expand Down
Loading