carlos / platform Public

Preserve signup birth evidence and immutable consent receipts

claude agent, operated by paul@keymail.dev 47d5e8c1e13b5395672013215eff8bef41b565e8 parent eb22913
8 files changed, +2620 −44
  • internal/console/account.go +4 −22
  • internal/console/account_test.go +89 −6
  • internal/console/billingmembership_test.go +1 −1
  • internal/console/overagepreferencemigration_test.go +479 −0
  • internal/console/overagepreferencestore.go +188 −12
  • internal/console/signuprecords.go +795 −0
  • internal/console/signuprecords_test.go +1050 −0
  • internal/console/store.go +14 −3

CI passed — run details

diff --git a/internal/console/account.go b/internal/console/account.go
index e7529eeda..4a49c7311 100644
--- a/internal/console/account.go
+++ b/internal/console/account.go
@@ -9,8 +9,6 @@ package console
import (
"context"
"encoding/json"
- "errors"
- "os"
"time"
"github.com/carlosframework/platform/internal/domains"
@@ -39,9 +37,8 @@ type accountManifest = domains.AccountManifest
// registry only ever reads this; it never assigns an id of its own, which is
// what makes the identity survive a registry rebuild.
//
-// Overwrite-safe (read etag, put, one retry), the same shape the fleet
-// snapshot publisher uses: the store's Put is CAS-only, so a rename racing
-// another of the owner's own tabs must retry rather than fail.
+// Backfill is create-only. A manifest that wins the race after the caller's
+// absence check is already the bucket's account identity and is left intact.
func WriteAccountManifest(ctx context.Context, s release.Store, id int64, sqid, name string, created time.Time) error {
body, err := json.Marshal(accountManifest{
ID: id, Sqid: sqid, Name: name, CreatedAt: created.UTC(),
@@ -49,21 +46,6 @@ func WriteAccountManifest(ctx context.Context, s release.Store, id int64, sqid,
if err != nil {
return err
}
- key := AccountManifestKey(sqid)
- put := func() error {
- etag := ""
- if _, cur, err := s.Get(ctx, key); err == nil {
- etag = cur
- } else if !errors.Is(err, os.ErrNotExist) {
- return err
- }
- _, err := s.Put(ctx, key, body, etag)
- return err
- }
- if err := put(); errors.Is(err, release.ErrCASConflict) {
- return put() // one retry on a lost race; a second loss surfaces
- } else if err != nil {
- return err
- }
- return nil
+ _, err = s.Put(ctx, AccountManifestKey(sqid), body, "")
+ return err
}
diff --git a/internal/console/account_test.go b/internal/console/account_test.go
index 2727cab6f..875bcfe74 100644
--- a/internal/console/account_test.go
+++ b/internal/console/account_test.go
@@ -1,8 +1,10 @@
package console
import (
+ "bytes"
"context"
"encoding/json"
+ "errors"
"strings"
"testing"
"time"
@@ -36,17 +38,98 @@ func TestWriteAccountManifestRoundTrips(t *testing.T) {
if m.ID != 42 || m.Sqid != "cdef" || m.Name != "Kass" || m.CreatedAt == "" {
t.Errorf("manifest = %+v", m)
}
- // Overwrite-safe: a rename must land, not lose to the CAS gate.
- if err := WriteAccountManifest(ctx, s, 42, "cdef", "Kassidy", time.Unix(1700000000, 0)); err != nil {
- t.Fatalf("rewrite: %v", err)
+ winner := append([]byte(nil), body...)
+ if err := WriteAccountManifest(ctx, s, 42, "cdef", "Kassidy", time.Unix(1700000000, 0)); !errors.Is(err, release.ErrCASConflict) {
+ t.Fatalf("rewrite error = %v, want CAS conflict", err)
}
body, _, _ = s.Get(ctx, "accounts/cdef/account.json")
- json.Unmarshal(body, &m)
- if m.Name != "Kassidy" {
- t.Errorf("rewrite lost the rename: %+v", m)
+ if !bytes.Equal(body, winner) {
+ t.Fatalf("refused rewrite changed manifest\ngot %s\nwant %s", body, winner)
}
}
+type accountBackfillRaceStore struct {
+ release.Store
+ putIfMatch []string
+ beforePut func(context.Context, release.Store)
+}
+
+func (s *accountBackfillRaceStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
+ s.putIfMatch = append(s.putIfMatch, ifMatch)
+ if before := s.beforePut; before != nil {
+ s.beforePut = nil
+ before(ctx, s.Store)
+ }
+ return s.Store.Put(ctx, key, body, ifMatch)
+}
+
+func TestEnsureAccountManifestBackfillIsCreateOnly(t *testing.T) {
+ created := time.Unix(1700000000, 123).UTC()
+ acct := &Account{ID: 42, Sqid: domains.Sqid(42), Name: "Local name", CreatedAt: created}
+ key := AccountManifestKey(acct.Sqid)
+ winner := []byte(`{"id":42,"sqid":"` + acct.Sqid + `","name":"Winner","created_at":"2026-10-03T23:00:00Z","owner":"winner@example.test","deleted_at":"2026-10-04T00:00:00Z","future_marker":{"opaque":true}}`)
+ assertWinner := func(t *testing.T, got []byte) {
+ t.Helper()
+ var manifest accountManifest
+ if err := json.Unmarshal(got, &manifest); err != nil {
+ t.Fatalf("decode winner: %v", err)
+ }
+ if manifest.Owner != "winner@example.test" || manifest.DeletedAt.IsZero() {
+ t.Fatalf("winner owner=%q deleted_at=%v", manifest.Owner, manifest.DeletedAt)
+ }
+ if !bytes.Equal(got, winner) {
+ t.Fatalf("winner bytes changed\ngot %s\nwant %s", got, winner)
+ }
+ }
+
+ t.Run("absent", func(t *testing.T) {
+ store := &accountBackfillRaceStore{Store: release.NewDirStore(t.TempDir())}
+ server := &Server{cfg: Config{Releases: store}}
+ server.ensureAccountManifest(t.Context(), acct)
+ body, _, err := store.Get(t.Context(), key)
+ if err != nil {
+ t.Fatal(err)
+ }
+ want, _ := json.Marshal(accountManifest{ID: acct.ID, Sqid: acct.Sqid, Name: acct.Name, CreatedAt: created})
+ if !bytes.Equal(body, want) || len(store.putIfMatch) != 1 || store.putIfMatch[0] != "" {
+ t.Fatalf("body=%s put if-match=%q, want body=%s one create-only put", body, store.putIfMatch, want)
+ }
+ })
+
+ t.Run("existing", func(t *testing.T) {
+ inner := release.NewDirStore(t.TempDir())
+ if _, err := inner.Put(t.Context(), key, winner, ""); err != nil {
+ t.Fatal(err)
+ }
+ store := &accountBackfillRaceStore{Store: inner}
+ (&Server{cfg: Config{Releases: store}}).ensureAccountManifest(t.Context(), acct)
+ got, _, err := inner.Get(t.Context(), key)
+ if err != nil || len(store.putIfMatch) != 0 {
+ t.Fatalf("winner=%s puts=%q err=%v", got, store.putIfMatch, err)
+ }
+ assertWinner(t, got)
+ })
+
+ t.Run("winner lands after absence precheck", func(t *testing.T) {
+ inner := release.NewDirStore(t.TempDir())
+ store := &accountBackfillRaceStore{Store: inner}
+ store.beforePut = func(ctx context.Context, inner release.Store) {
+ if _, err := inner.Put(ctx, key, winner, ""); err != nil {
+ t.Fatal(err)
+ }
+ }
+ (&Server{cfg: Config{Releases: store}}).ensureAccountManifest(t.Context(), acct)
+ got, _, err := inner.Get(t.Context(), key)
+ if err != nil {
+ t.Fatal(err)
+ }
+ assertWinner(t, got)
+ if len(store.putIfMatch) != 1 || store.putIfMatch[0] != "" {
+ t.Fatalf("backfill puts if-match=%q, want exactly one create-only attempt", store.putIfMatch)
+ }
+ })
+}
+
// The sqid a minted account carries must be the frozen encoder over its id —
// nothing else, ever, or a platform domain stops resolving to its account.
func TestMintedSqidIsTheFrozenEncoding(t *testing.T) {
diff --git a/internal/console/billingmembership_test.go b/internal/console/billingmembership_test.go
index 241cddf28..f9ea25a1e 100644
--- a/internal/console/billingmembership_test.go
+++ b/internal/console/billingmembership_test.go
@@ -1038,7 +1038,7 @@ func TestMembershipWriterInventory(t *testing.T) {
"internal/console/store.go:AddMember",
"internal/console/store.go:AdoptManifestOwner",
"internal/console/store.go:BackfillOpsMemberships",
- "internal/console/store.go:CreateAccount",
+ "internal/console/store.go:createAccountTx",
},
// Role only, between two existing members.
"update": {
diff --git a/internal/console/overagepreferencemigration_test.go b/internal/console/overagepreferencemigration_test.go
new file mode 100644
index 000000000..d681a2525
--- /dev/null
+++ b/internal/console/overagepreferencemigration_test.go
@@ -0,0 +1,479 @@
+package console
+
+import (
+ "database/sql"
+ "errors"
+ "fmt"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+)
+
+const oldOverageReceiptTable = `CREATE TABLE overage_preference_receipts (
+ operation_id TEXT PRIMARY KEY, account_id INTEGER NOT NULL,
+ actor TEXT NOT NULL, request_digest TEXT NOT NULL,
+ revision INTEGER NOT NULL, requested_on INTEGER NOT NULL,
+ terms_version TEXT NOT NULL, price_book TEXT NOT NULL,
+ currency TEXT NOT NULL, disclosure_digest TEXT NOT NULL,
+ offer_digest TEXT NOT NULL, changed_at INTEGER NOT NULL,
+ UNIQUE(account_id, revision)
+)`
+
+const overageReceiptColumns = `operation_id,account_id,actor,request_digest,revision,requested_on,terms_version,price_book,currency,disclosure_digest,offer_digest,changed_at`
+
+func downgradeOverageReceipts(t *testing.T, db *sql.DB) {
+ t.Helper()
+ for _, name := range []string{"overage_preference_receipts_no_update", "overage_preference_receipts_no_delete", "overage_preference_receipts_no_replace"} {
+ if _, err := db.Exec(`DROP TRIGGER ` + name); err != nil {
+ t.Fatal(err)
+ }
+ }
+ for _, stmt := range []string{
+ `ALTER TABLE overage_preference_receipts RENAME TO receipts_new_shape`,
+ oldOverageReceiptTable,
+ `INSERT INTO overage_preference_receipts(` + overageReceiptColumns + `) SELECT ` + overageReceiptColumns + ` FROM receipts_new_shape`,
+ `DROP TABLE receipts_new_shape`,
+ } {
+ if _, err := db.Exec(stmt); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := createOverageReceiptTriggers(db, false); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func oldReceiptFixture(t *testing.T, path string) *sql.DB {
+ t.Helper()
+ s, err := Open(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := s.Close(); err != nil {
+ t.Fatal(err)
+ }
+ db, err := sql.Open("sqlite", path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ downgradeOverageReceipts(t, db)
+ return db
+}
+
+func receiptRows(t *testing.T, db *sql.DB) [][]string {
+ t.Helper()
+ rows, err := db.Query(`SELECT operation_id,typeof(operation_id),account_id,typeof(account_id),actor,typeof(actor),request_digest,typeof(request_digest),revision,typeof(revision),requested_on,typeof(requested_on),terms_version,typeof(terms_version),price_book,typeof(price_book),currency,typeof(currency),disclosure_digest,typeof(disclosure_digest),offer_digest,typeof(offer_digest),changed_at,typeof(changed_at) FROM main.overage_preference_receipts ORDER BY operation_id`)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer rows.Close()
+ var out [][]string
+ for rows.Next() {
+ values := make([]any, 24)
+ ptrs := make([]any, 24)
+ for i := range values {
+ ptrs[i] = &values[i]
+ }
+ if err := rows.Scan(ptrs...); err != nil {
+ t.Fatal(err)
+ }
+ line := make([]string, 24)
+ for i, value := range values {
+ line[i] = fmt.Sprintf("%T:%v", value, value)
+ }
+ out = append(out, line)
+ }
+ if err := rows.Err(); err != nil {
+ t.Fatal(err)
+ }
+ return out
+}
+
+func preferenceRows(t *testing.T, db *sql.DB) [][]string {
+ t.Helper()
+ rows, err := db.Query(`SELECT account_id,typeof(account_id),revision,typeof(revision),requested_on,typeof(requested_on),terms_version,typeof(terms_version),price_book,typeof(price_book),currency,typeof(currency),disclosure_digest,typeof(disclosure_digest),offer_digest,typeof(offer_digest),actor,typeof(actor),operation_id,typeof(operation_id),updated_at,typeof(updated_at) FROM overage_preferences ORDER BY account_id`)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer rows.Close()
+ var out [][]string
+ for rows.Next() {
+ values := make([]any, 22)
+ ptrs := make([]any, 22)
+ for i := range values {
+ ptrs[i] = &values[i]
+ }
+ if err := rows.Scan(ptrs...); err != nil {
+ t.Fatal(err)
+ }
+ line := make([]string, 22)
+ for i, value := range values {
+ line[i] = fmt.Sprintf("%T:%v", value, value)
+ }
+ out = append(out, line)
+ }
+ if err := rows.Err(); err != nil {
+ t.Fatal(err)
+ }
+ return out
+}
+
+func receiptSchema(t *testing.T, db *sql.DB) []string {
+ t.Helper()
+ rows, err := db.Query(`SELECT type,name,tbl_name,coalesce(sql,'') FROM sqlite_master WHERE name LIKE 'overage_preference_receipts%' ORDER BY type,name`)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer rows.Close()
+ var out []string
+ for rows.Next() {
+ var typ, name, table, sqlText string
+ if err := rows.Scan(&typ, &name, &table, &sqlText); err != nil {
+ t.Fatal(err)
+ }
+ out = append(out, typ+"\x00"+name+"\x00"+table+"\x00"+sqlText)
+ }
+ if err := rows.Err(); err != nil {
+ t.Fatal(err)
+ }
+ return out
+}
+
+func membershipSerialState(t *testing.T, db *sql.DB) (int, int64) {
+ t.Helper()
+ var rows int
+ var serial int64
+ if err := db.QueryRow(`SELECT count(*),coalesce(max(serial),0) FROM membership_write_serial WHERE id=0`).Scan(&rows, &serial); err != nil {
+ t.Fatal(err)
+ }
+ return rows, serial
+}
+
+func TestOverageReceiptRowIDMigrationPreservesHistory(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "console.db")
+ now := time.Date(2026, 10, 4, 5, 0, 0, 0, time.UTC)
+ s, err := Open(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ seedOverageAccount(t, s, 42, overageOwner, now)
+ on := overageChange(42, overageOwner, 0, true, 71)
+ if _, err := s.changeOveragePreference(on, now.Add(time.Second)); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.changeOveragePreference(overageChange(42, overageOwner, 1, false, 72), now.Add(2*time.Second)); err != nil {
+ t.Fatal(err)
+ }
+ if err := s.Close(); err != nil {
+ t.Fatal(err)
+ }
+
+ raw, err := sql.Open("sqlite", path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ downgradeOverageReceipts(t, raw)
+ before := receiptRows(t, raw)
+ preferenceBefore := preferenceRows(t, raw)
+ tx, err := raw.Begin()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := tx.Exec(`INSERT OR REPLACE INTO overage_preference_receipts(rowid,` + overageReceiptColumns + `) SELECT rowid,operation_id||'x',account_id+1,actor,request_digest,revision+10,requested_on,terms_version,price_book,currency,disclosure_digest,offer_digest,changed_at FROM overage_preference_receipts LIMIT 1`); err != nil {
+ t.Fatalf("old-schema defect witness did not replace by rowid: %v", err)
+ }
+ _ = tx.Rollback()
+ if err := raw.Close(); err != nil {
+ t.Fatal(err)
+ }
+
+ s, err = Open(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer s.Close()
+ if got := receiptRows(t, s.db); !reflect.DeepEqual(got, before) {
+ t.Fatalf("receipt rows changed\ngot %#v\nwant %#v", got, before)
+ }
+ if got := preferenceRows(t, s.db); !reflect.DeepEqual(got, preferenceBefore) {
+ t.Fatalf("current preference changed\ngot %#v\nwant %#v", got, preferenceBefore)
+ }
+ var wr, triggers int
+ if err := s.db.QueryRow(`SELECT wr FROM pragma_table_list WHERE schema='main' AND name='overage_preference_receipts'`).Scan(&wr); err != nil {
+ t.Fatal(err)
+ }
+ if err := s.db.QueryRow(`SELECT count(*) FROM sqlite_master WHERE type='trigger' AND tbl_name='overage_preference_receipts'`).Scan(&triggers); err != nil {
+ t.Fatal(err)
+ }
+ if wr != 1 || triggers != 3 {
+ t.Fatalf("wr=%d triggers=%d", wr, triggers)
+ }
+ replayed, err := s.changeOveragePreference(on, now.Add(3*time.Second))
+ if err != nil || replayed.Revision != 1 || !replayed.RequestedOn {
+ t.Fatalf("replay=%+v err=%v", replayed, err)
+ }
+ current, err := s.getOveragePreference(42, overageOwner)
+ if err != nil || current.Revision != 2 || current.RequestedOn {
+ t.Fatalf("current=%+v err=%v", current, err)
+ }
+}
+
+func TestOverageReceiptWithoutRowIDRefusesMutationRoutes(t *testing.T) {
+ s := newOverageStore(t, filepath.Join(t.TempDir(), "console.db"))
+ now := time.Date(2026, 10, 4, 6, 0, 0, 0, time.UTC)
+ seedOverageAccount(t, s, 42, overageOwner, now)
+ if _, err := s.changeOveragePreference(overageChange(42, overageOwner, 0, false, 73), now); err != nil {
+ t.Fatal(err)
+ }
+ want := receiptRows(t, s.db)
+ replacementValues := `operation_id||'x',account_id+100,actor,request_digest,revision+100,requested_on,terms_version,price_book,currency,disclosure_digest,offer_digest,changed_at`
+ statements := []string{
+ `INSERT OR REPLACE INTO overage_preference_receipts(rowid,` + overageReceiptColumns + `) SELECT 1,` + replacementValues + ` FROM overage_preference_receipts`,
+ `INSERT OR REPLACE INTO overage_preference_receipts(_rowid_,` + overageReceiptColumns + `) SELECT 1,` + replacementValues + ` FROM overage_preference_receipts`,
+ `INSERT OR REPLACE INTO overage_preference_receipts(oid,` + overageReceiptColumns + `) SELECT 1,` + replacementValues + ` FROM overage_preference_receipts`,
+ `UPDATE OR REPLACE overage_preference_receipts SET actor='changed'`,
+ `INSERT OR REPLACE INTO overage_preference_receipts(` + overageReceiptColumns + `) SELECT operation_id,account_id+100,actor,request_digest,revision+100,requested_on,terms_version,price_book,currency,disclosure_digest,offer_digest,changed_at FROM overage_preference_receipts`,
+ `INSERT OR REPLACE INTO overage_preference_receipts(` + overageReceiptColumns + `) SELECT operation_id||'x',account_id,actor,request_digest,revision,requested_on,terms_version,price_book,currency,disclosure_digest,offer_digest,changed_at FROM overage_preference_receipts`,
+ `INSERT INTO overage_preference_receipts(` + overageReceiptColumns + `) SELECT ` + overageReceiptColumns + ` FROM overage_preference_receipts ON CONFLICT(operation_id) DO UPDATE SET actor='changed'`,
+ `UPDATE overage_preference_receipts SET actor='changed'`,
+ `DELETE FROM overage_preference_receipts`,
+ }
+ for _, stmt := range statements {
+ if _, err := s.db.Exec(stmt); err == nil {
+ t.Errorf("mutation succeeded: %s", stmt)
+ }
+ if got := receiptRows(t, s.db); !reflect.DeepEqual(got, want) {
+ t.Fatalf("mutation changed rows: %s", stmt)
+ }
+ }
+ // The frozen old migration remains a no-op against the new table.
+ if _, err := s.db.Exec(`CREATE TABLE IF NOT EXISTS ` + strings.TrimPrefix(oldOverageReceiptTable, "CREATE TABLE ")); err != nil {
+ t.Fatal(err)
+ }
+ for _, trigger := range overageReceiptTriggers {
+ if _, err := s.db.Exec(strings.Replace(trigger, "CREATE TRIGGER ", "CREATE TRIGGER IF NOT EXISTS ", 1)); err != nil {
+ t.Fatal(err)
+ }
+ }
+}
+
+func TestOverageReceiptMigrationRefusesUnknownOrInvalidOldState(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ contaminate func(*testing.T, *sql.DB)
+ }{
+ {"null operation id", func(t *testing.T, db *sql.DB) {
+ _, err := db.Exec(`INSERT INTO overage_preference_receipts(` + overageReceiptColumns + `) VALUES(NULL,1,'actor','digest',1,0,'','','','','',1)`)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }},
+ {"reserved replacement", func(t *testing.T, db *sql.DB) {
+ if _, err := db.Exec(`CREATE TABLE ` + overageReceiptReplacement + `(value TEXT)`); err != nil {
+ t.Fatal(err)
+ }
+ }},
+ {"unknown index", func(t *testing.T, db *sql.DB) {
+ if _, err := db.Exec(`CREATE INDEX unknown_receipt_index ON overage_preference_receipts(actor)`); err != nil {
+ t.Fatal(err)
+ }
+ }},
+ {"trigger name collision", func(t *testing.T, db *sql.DB) {
+ if _, err := db.Exec(`DROP TRIGGER overage_preference_receipts_no_update`); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := db.Exec(`CREATE TABLE other_receipts(value TEXT)`); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := db.Exec(`CREATE TRIGGER overage_preference_receipts_no_update BEFORE UPDATE ON other_receipts BEGIN SELECT 1; END`); err != nil {
+ t.Fatal(err)
+ }
+ }},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "console.db")
+ db := oldReceiptFixture(t, path)
+ tc.contaminate(t, db)
+ var schema string
+ if err := db.QueryRow(`SELECT sql FROM sqlite_master WHERE type='table' AND name='overage_preference_receipts'`).Scan(&schema); err != nil {
+ t.Fatal(err)
+ }
+ if err := db.Close(); err != nil {
+ t.Fatal(err)
+ }
+ if s, err := Open(path); err == nil {
+ s.Close()
+ t.Fatal("Open accepted unsafe old state")
+ }
+ check, err := sql.Open("sqlite", path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer check.Close()
+ var got string
+ if err := check.QueryRow(`SELECT sql FROM sqlite_master WHERE type='table' AND name='overage_preference_receipts'`).Scan(&got); err != nil {
+ t.Fatal(err)
+ }
+ if got != schema {
+ t.Fatalf("rollback changed old schema\ngot %s\nwant %s", got, schema)
+ }
+ var replacements int
+ if err := check.QueryRow(`SELECT count(*) FROM sqlite_master WHERE name=?`, overageReceiptReplacement).Scan(&replacements); err != nil {
+ t.Fatal(err)
+ }
+ if tc.name != "reserved replacement" && replacements != 0 {
+ t.Fatalf("replacement survived rollback: %d", replacements)
+ }
+ })
+ }
+}
+
+func TestOverageReceiptMigrationConcurrentAndIdempotent(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "console.db")
+ if err := oldReceiptFixture(t, path).Close(); err != nil {
+ t.Fatal(err)
+ }
+ start := make(chan struct{})
+ var wg sync.WaitGroup
+ errs := make(chan error, 2)
+ for range 2 {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ <-start
+ s, err := Open(path)
+ if err == nil {
+ err = s.Close()
+ }
+ errs <- err
+ }()
+ }
+ close(start)
+ wg.Wait()
+ close(errs)
+ for openErr := range errs {
+ if openErr != nil {
+ retry, err := Open(path)
+ if err != nil {
+ t.Fatalf("concurrent Open error %v; retry: %v", openErr, err)
+ }
+ if err := retry.Close(); err != nil {
+ t.Fatal(err)
+ }
+ }
+ }
+ s, err := Open(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var schema string
+ var serial int64
+ if err := s.db.QueryRow(`SELECT sql FROM sqlite_master WHERE type='table' AND name='overage_preference_receipts'`).Scan(&schema); err != nil {
+ t.Fatal(err)
+ }
+ if err := s.db.QueryRow(`SELECT serial FROM membership_write_serial WHERE id=0`).Scan(&serial); err != nil {
+ t.Fatal(err)
+ }
+ if err := s.Close(); err != nil {
+ t.Fatal(err)
+ }
+ s, err = Open(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer s.Close()
+ var schema2 string
+ var serial2 int64
+ if err := s.db.QueryRow(`SELECT sql FROM sqlite_master WHERE type='table' AND name='overage_preference_receipts'`).Scan(&schema2); err != nil {
+ t.Fatal(err)
+ }
+ if err := s.db.QueryRow(`SELECT serial FROM membership_write_serial WHERE id=0`).Scan(&serial2); err != nil {
+ t.Fatal(err)
+ }
+ if schema2 != schema || serial2 != serial {
+ t.Fatalf("idempotent reopen changed schema/serial: serial %d -> %d", serial, serial2)
+ }
+}
+
+func TestOverageReceiptMigrationRollsBackDropWhenRenameFindsMalformedView(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "console.db")
+ db := oldReceiptFixture(t, path)
+ defer db.Close()
+ if _, err := db.Exec(`INSERT INTO overage_preference_receipts(` + overageReceiptColumns + `) VALUES('operation',42,'actor','digest',1,0,'terms','book','eur','disclosure','offer',123)`); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := db.Exec(`CREATE VIEW malformed_receipt_dependency AS SELECT * FROM table_that_does_not_exist`); err != nil {
+ t.Fatal(err)
+ }
+ wantRows := receiptRows(t, db)
+ wantSchema := receiptSchema(t, db)
+ wantSerialRows, wantSerial := membershipSerialState(t, db)
+
+ s := &Store{db: db}
+ err := s.rebuildOverageReceipts()
+ if err == nil || !strings.Contains(err.Error(), "error in view malformed_receipt_dependency") {
+ t.Fatalf("rebuild error = %v, want ALTER rename malformed-view failure", err)
+ }
+ if got := receiptRows(t, db); !reflect.DeepEqual(got, wantRows) {
+ t.Fatalf("rollback changed rows\ngot %#v\nwant %#v", got, wantRows)
+ }
+ if got := receiptSchema(t, db); !reflect.DeepEqual(got, wantSchema) {
+ t.Fatalf("rollback changed schema\ngot %#v\nwant %#v", got, wantSchema)
+ }
+ serialRows, serial := membershipSerialState(t, db)
+ if serialRows != wantSerialRows || serial != wantSerial {
+ t.Fatalf("rollback changed membership serial (%d,%d) -> (%d,%d)", wantSerialRows, wantSerial, serialRows, serial)
+ }
+ if _, err := db.Exec(`DROP VIEW malformed_receipt_dependency`); err != nil {
+ t.Fatal(err)
+ }
+ if err := s.rebuildOverageReceipts(); err != nil {
+ t.Fatalf("clean retry: %v", err)
+ }
+ var wr int
+ if err := db.QueryRow(`SELECT wr FROM pragma_table_list WHERE schema='main' AND name='overage_preference_receipts'`).Scan(&wr); err != nil || wr != 1 {
+ t.Fatalf("clean retry wr=%d err=%v", wr, err)
+ }
+}
+
+func TestOverageReceiptMigrationRefusesSameConnectionTEMPShadow(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "console.db")
+ s, err := Open(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer s.Close()
+ now := time.Date(2026, 10, 4, 7, 0, 0, 0, time.UTC)
+ seedOverageAccount(t, s, 42, overageOwner, now)
+ if _, err := s.changeOveragePreference(overageChange(42, overageOwner, 0, false, 74), now); err != nil {
+ t.Fatal(err)
+ }
+ wantRows := receiptRows(t, s.db)
+ wantSchema := receiptSchema(t, s.db)
+ wantSerialRows, wantSerial := membershipSerialState(t, s.db)
+ if _, err := s.db.Exec(`CREATE TEMP TABLE overage_preference_receipts(value TEXT)`); err != nil {
+ t.Fatal(err)
+ }
+ if err := s.migrateOveragePreference(); !errors.Is(err, errOveragePreferenceCorrupt) {
+ t.Fatalf("TEMP-shadow migration error = %v", err)
+ }
+ if got := receiptRows(t, s.db); !reflect.DeepEqual(got, wantRows) {
+ t.Fatalf("TEMP refusal changed main rows\ngot %#v\nwant %#v", got, wantRows)
+ }
+ if got := receiptSchema(t, s.db); !reflect.DeepEqual(got, wantSchema) {
+ t.Fatalf("TEMP refusal changed main schema\ngot %#v\nwant %#v", got, wantSchema)
+ }
+ serialRows, serial := membershipSerialState(t, s.db)
+ if serialRows != wantSerialRows || serial != wantSerial {
+ t.Fatalf("TEMP refusal changed membership serial (%d,%d) -> (%d,%d)", wantSerialRows, wantSerial, serialRows, serial)
+ }
+ if _, err := s.db.Exec(`DROP TABLE temp.overage_preference_receipts`); err != nil {
+ t.Fatal(err)
+ }
+ if err := s.migrateOveragePreference(); err != nil {
+ t.Fatalf("migration after TEMP cleanup: %v", err)
+ }
+}
diff --git a/internal/console/overagepreferencestore.go b/internal/console/overagepreferencestore.go
index 2da74e953..eba0cea4b 100644
--- a/internal/console/overagepreferencestore.go
+++ b/internal/console/overagepreferencestore.go
@@ -7,13 +7,35 @@ import (
"encoding/hex"
"errors"
"math"
+ "reflect"
"regexp"
+ "strings"
"time"
"unicode/utf8"
"github.com/carlosframework/platform/internal/maintenanceguard"
)
+const overageReceiptReplacement = "overage_preference_receipts_without_rowid"
+
+var overageReceiptTriggers = []string{
+ `CREATE TRIGGER overage_preference_receipts_no_update BEFORE UPDATE ON overage_preference_receipts BEGIN SELECT RAISE(ABORT,'overage_preference_receipt_immutable'); END`,
+ `CREATE TRIGGER overage_preference_receipts_no_delete BEFORE DELETE ON overage_preference_receipts BEGIN SELECT RAISE(ABORT,'overage_preference_receipt_immutable'); END`,
+ `CREATE TRIGGER overage_preference_receipts_no_replace BEFORE INSERT ON overage_preference_receipts
+ WHEN EXISTS(SELECT 1 FROM overage_preference_receipts WHERE operation_id=NEW.operation_id OR (account_id=NEW.account_id AND revision=NEW.revision))
+ BEGIN SELECT RAISE(ABORT,'overage_preference_receipt_immutable'); END`,
+}
+
+const overageReceiptTable = `CREATE TABLE overage_preference_receipts (
+ operation_id TEXT PRIMARY KEY, account_id INTEGER NOT NULL,
+ actor TEXT NOT NULL, request_digest TEXT NOT NULL,
+ revision INTEGER NOT NULL, requested_on INTEGER NOT NULL,
+ terms_version TEXT NOT NULL, price_book TEXT NOT NULL,
+ currency TEXT NOT NULL, disclosure_digest TEXT NOT NULL,
+ offer_digest TEXT NOT NULL, changed_at INTEGER NOT NULL,
+ UNIQUE(account_id, revision)
+) WITHOUT ROWID`
+
var (
errOveragePreferenceInvalid = errors.New("overage_preference_invalid")
errOveragePreferenceDenied = errors.New("overage_preference_denied")
@@ -72,7 +94,12 @@ func (s *Store) migrateOveragePreference() error {
disclosure_digest TEXT NOT NULL, offer_digest TEXT NOT NULL,
actor TEXT NOT NULL, operation_id TEXT NOT NULL, updated_at INTEGER NOT NULL
)`,
- `CREATE TABLE IF NOT EXISTS overage_preference_receipts (
+ } {
+ if _, err := s.db.Exec(stmt); err != nil {
+ return err
+ }
+ }
+ if _, err := s.db.Exec(`CREATE TABLE IF NOT EXISTS overage_preference_receipts (
operation_id TEXT PRIMARY KEY, account_id INTEGER NOT NULL,
actor TEXT NOT NULL, request_digest TEXT NOT NULL,
revision INTEGER NOT NULL, requested_on INTEGER NOT NULL,
@@ -80,20 +107,161 @@ func (s *Store) migrateOveragePreference() error {
currency TEXT NOT NULL, disclosure_digest TEXT NOT NULL,
offer_digest TEXT NOT NULL, changed_at INTEGER NOT NULL,
UNIQUE(account_id, revision)
-)`,
- `CREATE TRIGGER IF NOT EXISTS overage_preference_receipts_no_update BEFORE UPDATE ON overage_preference_receipts BEGIN SELECT RAISE(ABORT,'overage_preference_receipt_immutable'); END`,
- `CREATE TRIGGER IF NOT EXISTS overage_preference_receipts_no_delete BEFORE DELETE ON overage_preference_receipts BEGIN SELECT RAISE(ABORT,'overage_preference_receipt_immutable'); END`,
- `CREATE TRIGGER IF NOT EXISTS overage_preference_receipts_no_replace BEFORE INSERT ON overage_preference_receipts
- WHEN EXISTS(SELECT 1 FROM overage_preference_receipts WHERE operation_id=NEW.operation_id OR (account_id=NEW.account_id AND revision=NEW.revision))
- BEGIN SELECT RAISE(ABORT,'overage_preference_receipt_immutable'); END`,
- } {
- if _, err := s.db.Exec(stmt); err != nil {
+) WITHOUT ROWID`); err != nil {
+ return err
+ }
+ wr, err := overageReceiptWR(s.db)
+ if err != nil {
+ return err
+ }
+ if wr == 1 {
+ if err := validateOverageReceiptShape(s.db, true); err != nil {
+ return err
+ }
+ }
+ if wr == 0 {
+ if err := s.rebuildOverageReceipts(); err != nil {
+ return err
+ }
+ }
+ return createOverageReceiptTriggers(s.db, true)
+}
+
+type overageReceiptExecer interface {
+ Exec(string, ...any) (sql.Result, error)
+}
+
+func createOverageReceiptTriggers(db overageReceiptExecer, ifNotExists bool) error {
+ for _, stmt := range overageReceiptTriggers {
+ if ifNotExists {
+ stmt = strings.Replace(stmt, "CREATE TRIGGER ", "CREATE TRIGGER IF NOT EXISTS ", 1)
+ }
+ if _, err := db.Exec(stmt); err != nil {
return err
}
}
return nil
}
+func overageReceiptWR(q interface{ QueryRow(string, ...any) *sql.Row }) (int, error) {
+ var wr int
+ err := q.QueryRow(`SELECT wr FROM pragma_table_list WHERE schema='main' AND name='overage_preference_receipts'`).Scan(&wr)
+ return wr, err
+}
+
+func (s *Store) rebuildOverageReceipts() (err error) {
+ tx, err := s.db.Begin()
+ if err != nil {
+ return err
+ }
+ defer func() { _ = tx.Rollback() }()
+ if _, err := lockMembershipWritesTx(tx); err != nil {
+ return err
+ }
+ wr, err := overageReceiptWR(tx)
+ if err != nil {
+ return err
+ }
+ if wr == 1 {
+ return nil
+ }
+ if err := validateOverageReceiptShape(tx, false); err != nil {
+ return err
+ }
+ create := strings.Replace(overageReceiptTable, "overage_preference_receipts", overageReceiptReplacement, 1)
+ if _, err := tx.Exec(create); err != nil {
+ return err
+ }
+ const columns = `operation_id,account_id,actor,request_digest,revision,requested_on,terms_version,price_book,currency,disclosure_digest,offer_digest,changed_at`
+ if _, err := tx.Exec(`INSERT INTO ` + overageReceiptReplacement + `(` + columns + `) SELECT ` + columns + ` FROM overage_preference_receipts`); err != nil {
+ return err
+ }
+ if _, err := tx.Exec(`DROP TABLE overage_preference_receipts`); err != nil {
+ return err
+ }
+ if _, err := tx.Exec(`ALTER TABLE ` + overageReceiptReplacement + ` RENAME TO overage_preference_receipts`); err != nil {
+ return err
+ }
+ if err := createOverageReceiptTriggers(tx, false); err != nil {
+ return err
+ }
+ return tx.Commit()
+}
+
+type overageReceiptQueryer interface {
+ Query(string, ...any) (*sql.Rows, error)
+ QueryRow(string, ...any) *sql.Row
+}
+
+func validateOverageReceiptShape(tx overageReceiptQueryer, withoutRowID bool) error {
+ var temp int
+ if err := tx.QueryRow(`SELECT count(*) FROM sqlite_temp_master WHERE name IN ('overage_preference_receipts','` + overageReceiptReplacement + `')`).Scan(&temp); err != nil || temp != 0 {
+ if err != nil {
+ return err
+ }
+ return errOveragePreferenceCorrupt
+ }
+ type column struct {
+ name, typ string
+ notnull, pk int
+ }
+ pkNotNull := 0
+ if withoutRowID {
+ pkNotNull = 1
+ }
+ want := []column{{"operation_id", "TEXT", pkNotNull, 1}, {"account_id", "INTEGER", 1, 0}, {"actor", "TEXT", 1, 0}, {"request_digest", "TEXT", 1, 0}, {"revision", "INTEGER", 1, 0}, {"requested_on", "INTEGER", 1, 0}, {"terms_version", "TEXT", 1, 0}, {"price_book", "TEXT", 1, 0}, {"currency", "TEXT", 1, 0}, {"disclosure_digest", "TEXT", 1, 0}, {"offer_digest", "TEXT", 1, 0}, {"changed_at", "INTEGER", 1, 0}}
+ rows, err := tx.Query(`PRAGMA main.table_info('overage_preference_receipts')`)
+ if err != nil {
+ return err
+ }
+ var got []column
+ for rows.Next() {
+ var cid int
+ var c column
+ var dflt any
+ if err := rows.Scan(&cid, &c.name, &c.typ, &c.notnull, &dflt, &c.pk); err != nil {
+ rows.Close()
+ return err
+ }
+ got = append(got, c)
+ }
+ if err := rows.Err(); err != nil {
+ rows.Close()
+ return err
+ }
+ if err := rows.Close(); err != nil {
+ return err
+ }
+ if !reflect.DeepEqual(got, want) {
+ return errOveragePreferenceCorrupt
+ }
+ objects, err := tx.Query(`SELECT type,name FROM sqlite_master WHERE tbl_name='overage_preference_receipts' AND type IN ('index','trigger')`)
+ if err != nil {
+ return err
+ }
+ known := map[string]bool{"overage_preference_receipts_no_update": true, "overage_preference_receipts_no_delete": true, "overage_preference_receipts_no_replace": true}
+ for objects.Next() {
+ var typ, name string
+ if err := objects.Scan(&typ, &name); err != nil {
+ objects.Close()
+ return err
+ }
+ if typ == "index" && strings.HasPrefix(name, "sqlite_autoindex_overage_preference_receipts_") {
+ continue
+ }
+ if typ == "trigger" && known[name] {
+ continue
+ }
+ objects.Close()
+ return errOveragePreferenceCorrupt
+ }
+ if err := objects.Err(); err != nil {
+ objects.Close()
+ return err
+ }
+ return objects.Close()
+}
+
func validOverageEvidence(e overageEvidence) bool {
return overageEvidenceName.MatchString(e.TermsVersion) &&
overageEvidenceName.MatchString(e.PriceBook) &&
@@ -276,6 +444,17 @@ func (s *Store) changeOveragePreference(c overagePreferenceChange, now time.Time
if err := authorizeOverageOwnerTx(tx, c.AccountID, c.Actor); err != nil {
return overagePreferenceReceipt{}, err
}
+ r, err := changeOveragePreferenceTx(tx, c, now)
+ if err != nil {
+ return overagePreferenceReceipt{}, err
+ }
+ if err := tx.Commit(); err != nil {
+ return overagePreferenceReceipt{}, err
+ }
+ return r, nil
+}
+
+func changeOveragePreferenceTx(tx *sql.Tx, c overagePreferenceChange, now time.Time) (overagePreferenceReceipt, error) {
current, err := loadOveragePreferenceTx(tx, c.AccountID)
if err != nil {
return overagePreferenceReceipt{}, err
@@ -319,9 +498,6 @@ func (s *Store) changeOveragePreference(c overagePreferenceChange, now time.Time
c.Evidence.DisclosureDigest, c.Evidence.OfferDigest, changed.Unix()); err != nil {
return overagePreferenceReceipt{}, err
}
- if err := tx.Commit(); err != nil {
- return overagePreferenceReceipt{}, err
- }
return overagePreferenceReceipt{OperationID: c.OperationID, AccountID: c.AccountID, Actor: c.Actor,
Revision: nextRevision, RequestedOn: c.RequestedOn, Evidence: c.Evidence, ChangedAt: changed}, nil
}
diff --git a/internal/console/signuprecords.go b/internal/console/signuprecords.go
new file mode 100644
index 000000000..2eeabc2d7
--- /dev/null
+++ b/internal/console/signuprecords.go
@@ -0,0 +1,795 @@
+package console
+
+import (
+ "bytes"
+ "crypto/rand"
+ "crypto/sha256"
+ "database/sql"
+ "encoding/binary"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "os"
+ "time"
+
+ "github.com/carlosframework/platform/internal/domains"
+ "github.com/carlosframework/platform/internal/maintenanceguard"
+)
+
+// These unmounted records bind caller-supplied scope and offer bytes. They do
+// not validate a backend, build an offer, or establish database continuity.
+type signupHeader struct {
+ ID, OperationID, Subject, Name, PreferenceOperationID string
+ Scope []byte
+ CreatedAt time.Time
+}
+type signupOffer struct {
+ Payload []byte
+ Evidence overageEvidence
+ ExpiresAt time.Time
+}
+type signupCeremony struct {
+ Subject string
+ AuthTime, RecordedAt int64
+ WindowSeconds int64
+}
+type signupSeal struct {
+ ChoicePresent, RequestedOn bool
+ OfferDigest string
+ Ceremony signupCeremony
+}
+type signupIntent struct {
+ Header signupHeader
+ Offer *signupOffer
+ Seal *signupSeal
+ Digest, Phase string
+ Attempt int64
+}
+type signupAttempt struct {
+ IntentID string
+ Number, AccountID int64
+ Sqid, Key, BindingDigest, BodyDigest string
+ Body []byte
+ CreatedAt time.Time
+}
+type signupObservation struct {
+ AccountID int64
+ Sqid, Key, IntentID, BindingDigest string
+ Attempt int64
+ CreatedAt time.Time
+ Body []byte
+ ETag string
+ Retired bool
+}
+type signupAttemptRecord struct {
+ Tuple signupAttempt
+ State string
+ Observation *signupObservation
+}
+type signupBirth struct {
+ IntentID, IntentDigest, BindingDigest, ScopeDigest string
+ Attempt, AccountID int64
+ Sqid, Name, Owner, Outcome string
+ CreatedAt, CommittedAt time.Time
+ Preference *overagePreferenceReceipt
+ PreferenceDigest string
+}
+type signupBirthResult struct {
+ Historical signupBirth
+ Current Account
+ Preference overagePreference
+}
+
+func signupDigest(domain string, fields ...[]byte) string {
+ h := sha256.New()
+ h.Write([]byte("carlos-signup-" + domain + "-v1\x00"))
+ var n [8]byte
+ for _, f := range fields {
+ binary.BigEndian.PutUint64(n[:], uint64(len(f)))
+ h.Write(n[:])
+ h.Write(f)
+ }
+ return hex.EncodeToString(h.Sum(nil))
+}
+func signupBytes(v any) []byte { b, _ := json.Marshal(v); return b }
+func signupDecode(b []byte, v any) error {
+ if err := json.Unmarshal(b, v); err != nil || !bytes.Equal(b, signupBytes(v)) {
+ return errOveragePreferenceCorrupt
+ }
+ return nil
+}
+func signupNow(now time.Time) bool { return !now.IsZero() && now.Unix() > 0 }
+func signupID() (string, error) {
+ var b [32]byte
+ if _, err := rand.Read(b[:]); err != nil {
+ return "", err
+ }
+ return hex.EncodeToString(b[:]), nil
+}
+func signupIdentity(id int64, sqid string) bool {
+ return id > domains.ReservedAccountID && sqid == domains.Sqid(id)
+}
+
+func (s *Store) migrateSignupRecords() error {
+ statements := []string{
+ `CREATE TABLE IF NOT EXISTS signup_intents (
+ id TEXT PRIMARY KEY, scope BLOB NOT NULL, subject TEXT NOT NULL, operation_id TEXT NOT NULL,
+ header BLOB NOT NULL, offer BLOB NOT NULL DEFAULT X'', seal BLOB NOT NULL DEFAULT X'',
+ digest TEXT NOT NULL DEFAULT '', phase TEXT NOT NULL CHECK(phase IN ('draft','prepared','cancelled','completed','retired')),
+ attempt INTEGER NOT NULL DEFAULT 0 CHECK(attempt>=0), UNIQUE(scope,subject,operation_id)) WITHOUT ROWID`,
+ `CREATE TABLE IF NOT EXISTS signup_mint_attempts (
+ intent_id TEXT NOT NULL, number INTEGER NOT NULL CHECK(number>0), account_id INTEGER NOT NULL,
+ tuple BLOB NOT NULL, state TEXT NOT NULL CHECK(state IN ('prepared','held','collision','observed_live','observed_retired')),
+ observation BLOB NOT NULL DEFAULT X'', PRIMARY KEY(intent_id,number)) WITHOUT ROWID`,
+ `CREATE TABLE IF NOT EXISTS account_birth_receipts (
+ intent_id TEXT PRIMARY KEY, scope BLOB NOT NULL, account_id INTEGER NOT NULL, sqid TEXT NOT NULL,
+ receipt BLOB NOT NULL, digest TEXT NOT NULL, UNIQUE(scope,account_id), UNIQUE(scope,sqid)) WITHOUT ROWID`,
+ `CREATE TRIGGER IF NOT EXISTS signup_intents_no_replace BEFORE INSERT ON signup_intents
+ WHEN EXISTS(SELECT 1 FROM signup_intents WHERE id=NEW.id OR (scope=NEW.scope AND subject=NEW.subject AND operation_id=NEW.operation_id))
+ BEGIN SELECT RAISE(ABORT,'signup_immutable'); END`,
+ `CREATE TRIGGER IF NOT EXISTS signup_intents_no_delete BEFORE DELETE ON signup_intents BEGIN SELECT RAISE(ABORT,'signup_immutable'); END`,
+ `CREATE TRIGGER IF NOT EXISTS signup_intents_update BEFORE UPDATE ON signup_intents
+ WHEN NEW.id IS NOT OLD.id OR NEW.scope IS NOT OLD.scope OR NEW.subject IS NOT OLD.subject OR NEW.operation_id IS NOT OLD.operation_id OR NEW.header IS NOT OLD.header
+ OR NOT (
+ (NEW.offer IS OLD.offer AND NEW.seal IS OLD.seal AND NEW.digest IS OLD.digest AND NEW.attempt=OLD.attempt AND
+ (NEW.phase=OLD.phase OR (OLD.phase IN ('draft','prepared') AND NEW.phase='cancelled') OR (OLD.phase='prepared' AND NEW.phase IN ('completed','retired'))))
+ OR (OLD.phase='draft' AND NEW.phase='draft' AND length(OLD.offer)=0 AND length(NEW.offer)>0 AND NEW.seal IS OLD.seal AND NEW.digest IS OLD.digest AND NEW.attempt=OLD.attempt)
+ OR (OLD.phase='draft' AND NEW.phase='prepared' AND NEW.offer IS OLD.offer AND length(OLD.seal)=0 AND length(NEW.seal)>0 AND OLD.digest='' AND length(NEW.digest)=64 AND NEW.attempt=OLD.attempt)
+ OR (OLD.phase='prepared' AND NEW.phase='prepared' AND NEW.offer IS OLD.offer AND NEW.seal IS OLD.seal AND NEW.digest IS OLD.digest AND NEW.attempt=OLD.attempt+1)
+ ) BEGIN SELECT RAISE(ABORT,'signup_immutable'); END`,
+ `CREATE TRIGGER IF NOT EXISTS signup_attempts_no_replace BEFORE INSERT ON signup_mint_attempts
+ WHEN EXISTS(SELECT 1 FROM signup_mint_attempts WHERE intent_id=NEW.intent_id AND number=NEW.number)
+ BEGIN SELECT RAISE(ABORT,'signup_immutable'); END`,
+ `CREATE TRIGGER IF NOT EXISTS signup_attempts_no_delete BEFORE DELETE ON signup_mint_attempts BEGIN SELECT RAISE(ABORT,'signup_immutable'); END`,
+ `CREATE TRIGGER IF NOT EXISTS signup_attempts_update BEFORE UPDATE ON signup_mint_attempts
+ WHEN NEW.intent_id IS NOT OLD.intent_id OR NEW.number!=OLD.number OR NEW.account_id!=OLD.account_id OR NEW.tuple IS NOT OLD.tuple
+ OR NOT ((NEW.state=OLD.state AND NEW.observation IS OLD.observation)
+ OR (OLD.state IN ('prepared','held','observed_live') AND NEW.state IN ('held','observed_live','observed_retired','collision') AND (length(OLD.observation)=0 OR NEW.observation IS OLD.observation OR OLD.state IN ('held','observed_live'))))
+ BEGIN SELECT RAISE(ABORT,'signup_immutable'); END`,
+ `CREATE TRIGGER IF NOT EXISTS signup_birth_no_replace BEFORE INSERT ON account_birth_receipts
+ WHEN EXISTS(SELECT 1 FROM account_birth_receipts WHERE intent_id=NEW.intent_id OR (scope=NEW.scope AND (account_id=NEW.account_id OR sqid=NEW.sqid)))
+ BEGIN SELECT RAISE(ABORT,'signup_immutable'); END`,
+ `CREATE TRIGGER IF NOT EXISTS signup_birth_no_update BEFORE UPDATE ON account_birth_receipts BEGIN SELECT RAISE(ABORT,'signup_immutable'); END`,
+ `CREATE TRIGGER IF NOT EXISTS signup_birth_no_delete BEFORE DELETE ON account_birth_receipts BEGIN SELECT RAISE(ABORT,'signup_immutable'); END`,
+ }
+ for _, q := range statements {
+ if _, err := s.db.Exec(q); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func signupSessionTx(tx *sql.Tx, raw, subject string, now time.Time) error {
+ if raw == "" || !signupNow(now) || subject != domains.NormalizeAddress(subject) || !domains.ValidAddress(subject) {
+ return errOveragePreferenceDenied
+ }
+ var address string
+ var expires int64
+ err := tx.QueryRow(`SELECT address,expires_at FROM sessions WHERE token_hash=?`, hashToken(raw)).Scan(&address, &expires)
+ if errors.Is(err, sql.ErrNoRows) {
+ return errOveragePreferenceDenied
+ }
+ if err != nil {
+ return err
+ }
+ if domains.NormalizeAddress(address) != subject || now.Unix() > expires {
+ return errOveragePreferenceDenied
+ }
+ return nil
+}
+func signupFreshTx(tx *sql.Tx, raw, subject string, now time.Time) (signupCeremony, error) {
+ var c signupCeremony
+ err := tx.QueryRow(`SELECT address,auth_time,recorded_at FROM step_ups WHERE session_hash=?`, hashToken(raw)).Scan(&c.Subject, &c.AuthTime, &c.RecordedAt)
+ if errors.Is(err, sql.ErrNoRows) {
+ return c, errOveragePreferenceDenied
+ }
+ if err != nil {
+ return c, err
+ }
+ age := now.Sub(time.Unix(c.AuthTime, 0))
+ if domains.NormalizeAddress(c.Subject) != subject || c.AuthTime <= 0 || c.RecordedAt <= 0 || c.RecordedAt > now.Unix() || age < -stepUpSkew || age > StepUpWindow {
+ return c, errOveragePreferenceDenied
+ }
+ c.Subject = subject
+ c.WindowSeconds = int64(StepUpWindow / time.Second)
+ return c, nil
+}
+func (s *Store) signupTx() (*sql.Tx, error) {
+ tx, err := s.db.Begin()
+ if err != nil {
+ return nil, err
+ }
+ if _, err = lockMembershipWritesTx(tx); err != nil {
+ tx.Rollback()
+ return nil, err
+ }
+ return tx, nil
+}
+func loadSignupTx(tx *sql.Tx, id string) (signupIntent, error) {
+ var i signupIntent
+ var h, o, z, scope []byte
+ var subject, op string
+ err := tx.QueryRow(`SELECT header,offer,seal,digest,phase,attempt,scope,subject,operation_id FROM signup_intents WHERE id=?`, id).Scan(&h, &o, &z, &i.Digest, &i.Phase, &i.Attempt, &scope, &subject, &op)
+ if err != nil {
+ return i, err
+ }
+ if err = signupDecode(h, &i.Header); err != nil {
+ return i, err
+ }
+ if i.Header.ID != id || !bytes.Equal(i.Header.Scope, scope) || i.Header.Subject != subject || i.Header.OperationID != op || !overageDigest.MatchString(id) || !signupNow(i.Header.CreatedAt) || !domains.ValidAccountName(i.Header.Name) || !maintenanceguard.ValidID(i.Header.PreferenceOperationID) {
+ return i, errOveragePreferenceCorrupt
+ }
+ if len(o) > 0 {
+ i.Offer = &signupOffer{}
+ if err = signupDecode(o, i.Offer); err != nil {
+ return i, err
+ }
+ if len(i.Offer.Payload) == 0 || !validOverageEvidence(i.Offer.Evidence) || !signupNow(i.Offer.ExpiresAt) {
+ return i, errOveragePreferenceCorrupt
+ }
+ }
+ if len(z) > 0 {
+ i.Seal = &signupSeal{}
+ if err = signupDecode(z, i.Seal); err != nil {
+ return i, err
+ }
+ c := i.Seal.Ceremony
+ if c.Subject != subject || c.AuthTime <= 0 || c.RecordedAt <= 0 || c.WindowSeconds != int64(StepUpWindow/time.Second) || (i.Seal.RequestedOn && (!i.Seal.ChoicePresent || i.Offer == nil)) || i.Seal.OfferDigest != signupDigest("offer", o) || i.Digest != signupDigest("intent", h, o, z) {
+ return i, errOveragePreferenceCorrupt
+ }
+ } else if i.Digest != "" || (i.Phase != "draft" && i.Phase != "cancelled") || i.Attempt != 0 {
+ return i, errOveragePreferenceCorrupt
+ }
+ if i.Phase == "draft" && i.Seal != nil {
+ return i, errOveragePreferenceCorrupt
+ }
+ return i, nil
+}
+func authorizedSignupTx(tx *sql.Tx, raw, id string, scope []byte, now time.Time) (signupIntent, error) {
+ i, err := loadSignupTx(tx, id)
+ if errors.Is(err, sql.ErrNoRows) {
+ return i, errOveragePreferenceDenied
+ }
+ if err != nil {
+ return i, err
+ }
+ if !bytes.Equal(scope, i.Header.Scope) {
+ return signupIntent{}, errOveragePreferenceDenied
+ }
+ if err = signupSessionTx(tx, raw, i.Header.Subject, now); err != nil {
+ return signupIntent{}, err
+ }
+ if i.Phase == "completed" {
+ var accountID int64
+ if err = tx.QueryRow(`SELECT account_id FROM account_birth_receipts WHERE intent_id=?`, id).Scan(&accountID); err != nil {
+ return signupIntent{}, errOveragePreferenceCorrupt
+ }
+ if err = authorizeOverageOwnerTx(tx, accountID, i.Header.Subject); err != nil {
+ return signupIntent{}, err
+ }
+ }
+ return i, nil
+}
+func (s *Store) createSignupDraft(raw, operation, name string, scope []byte, now time.Time) (signupIntent, error) {
+ if !maintenanceguard.ValidID(operation) || !domains.ValidAccountName(name) || len(scope) == 0 || len(scope) > 65536 || !signupNow(now) {
+ return signupIntent{}, os.ErrInvalid
+ }
+ tx, err := s.signupTx()
+ if err != nil {
+ return signupIntent{}, err
+ }
+ defer tx.Rollback()
+ var subject string
+ if err = tx.QueryRow(`SELECT address FROM sessions WHERE token_hash=?`, hashToken(raw)).Scan(&subject); err != nil {
+ return signupIntent{}, errOveragePreferenceDenied
+ }
+ subject = domains.NormalizeAddress(subject)
+ if err = signupSessionTx(tx, raw, subject, now); err != nil {
+ return signupIntent{}, err
+ }
+ var id string
+ err = tx.QueryRow(`SELECT id FROM signup_intents WHERE scope=? AND subject=? AND operation_id=?`, scope, subject, operation).Scan(&id)
+ if err == nil {
+ i, e := authorizedSignupTx(tx, raw, id, scope, now)
+ if e != nil {
+ return signupIntent{}, e
+ }
+ if i.Header.Name != name {
+ return signupIntent{}, errOveragePreferenceConflict
+ }
+ return i, nil
+ }
+ if !errors.Is(err, sql.ErrNoRows) {
+ return signupIntent{}, err
+ }
+ id, err = signupID()
+ if err != nil {
+ return signupIntent{}, err
+ }
+ pref, err := randomToken()
+ if err != nil {
+ return signupIntent{}, err
+ }
+ i := signupIntent{Header: signupHeader{ID: id, OperationID: operation, Subject: subject, Name: name, Scope: bytes.Clone(scope), CreatedAt: now.UTC(), PreferenceOperationID: pref}, Phase: "draft"}
+ _, err = tx.Exec(`INSERT INTO signup_intents(id,scope,subject,operation_id,header,phase) VALUES(?,?,?,?,?,'draft')`, id, scope, subject, operation, signupBytes(i.Header))
+ if err != nil {
+ return signupIntent{}, err
+ }
+ return i, tx.Commit()
+}
+func (s *Store) freezeSignupOffer(raw, id string, scope []byte, o signupOffer, now time.Time) (signupIntent, error) {
+ if len(o.Payload) == 0 || len(o.Payload) > 1<<20 || !validOverageEvidence(o.Evidence) || !signupNow(o.ExpiresAt) {
+ return signupIntent{}, os.ErrInvalid
+ }
+ o.ExpiresAt = o.ExpiresAt.UTC()
+ tx, err := s.signupTx()
+ if err != nil {
+ return signupIntent{}, err
+ }
+ defer tx.Rollback()
+ i, err := authorizedSignupTx(tx, raw, id, scope, now)
+ if err != nil {
+ return signupIntent{}, err
+ }
+ if i.Offer != nil {
+ if !bytes.Equal(signupBytes(i.Offer), signupBytes(o)) {
+ return signupIntent{}, errOveragePreferenceConflict
+ }
+ return i, nil
+ }
+ if i.Phase != "draft" {
+ return signupIntent{}, errOveragePreferenceConflict
+ }
+ if _, err = tx.Exec(`UPDATE signup_intents SET offer=? WHERE id=? AND phase='draft' AND length(offer)=0`, signupBytes(o), id); err != nil {
+ return signupIntent{}, err
+ }
+ i.Offer = &o
+ return i, tx.Commit()
+}
+func (s *Store) sealSignup(raw, id string, scope []byte, present, on bool, offerDigest string, now time.Time) (signupIntent, error) {
+ if on && !present {
+ return signupIntent{}, os.ErrInvalid
+ }
+ tx, err := s.signupTx()
+ if err != nil {
+ return signupIntent{}, err
+ }
+ defer tx.Rollback()
+ i, err := authorizedSignupTx(tx, raw, id, scope, now)
+ if err != nil {
+ return signupIntent{}, err
+ }
+ if i.Seal != nil {
+ if i.Seal.ChoicePresent != present || i.Seal.RequestedOn != on || i.Seal.OfferDigest != offerDigest {
+ return signupIntent{}, errOveragePreferenceConflict
+ }
+ return i, nil
+ }
+ var o []byte
+ if i.Offer != nil {
+ o = signupBytes(i.Offer)
+ }
+ if i.Phase != "draft" || offerDigest != signupDigest("offer", o) {
+ return signupIntent{}, errOveragePreferenceConflict
+ }
+ if on && (i.Offer == nil || !now.Before(i.Offer.ExpiresAt)) {
+ return signupIntent{}, os.ErrInvalid
+ }
+ c, err := signupFreshTx(tx, raw, i.Header.Subject, now)
+ if err != nil {
+ return signupIntent{}, err
+ }
+ i.Seal = &signupSeal{ChoicePresent: present, RequestedOn: on, OfferDigest: offerDigest, Ceremony: c}
+ i.Digest = signupDigest("intent", signupBytes(i.Header), o, signupBytes(i.Seal))
+ i.Phase = "prepared"
+ _, err = tx.Exec(`UPDATE signup_intents SET seal=?,digest=?,phase='prepared' WHERE id=? AND phase='draft'`, signupBytes(i.Seal), i.Digest, id)
+ if err != nil {
+ return signupIntent{}, err
+ }
+ return i, tx.Commit()
+}
+func (s *Store) cancelSignup(raw, id string, scope []byte, now time.Time) error {
+ tx, err := s.signupTx()
+ if err != nil {
+ return err
+ }
+ defer tx.Rollback()
+ i, err := authorizedSignupTx(tx, raw, id, scope, now)
+ if err != nil {
+ return err
+ }
+ if i.Phase == "cancelled" {
+ return nil
+ }
+ if i.Phase != "draft" && i.Phase != "prepared" {
+ return errOveragePreferenceConflict
+ }
+ _, err = tx.Exec(`UPDATE signup_intents SET phase='cancelled' WHERE id=? AND phase IN ('draft','prepared')`, id)
+ if err != nil {
+ return err
+ }
+ return tx.Commit()
+}
+
+func signupAttemptBinding(i signupIntent, a signupAttempt) string {
+ return signupDigest("binding", []byte(i.Digest), signupBytes(struct {
+ Number, AccountID int64
+ Sqid, Key string
+ CreatedAt time.Time
+ }{a.Number, a.AccountID, a.Sqid, a.Key, a.CreatedAt}))
+}
+
+const signupMaxAttempts = 5
+
+func loadSignupCurrentAttemptTx(tx *sql.Tx, i signupIntent) (signupAttemptRecord, error) {
+ if i.Attempt < 1 || i.Attempt > signupMaxAttempts {
+ return signupAttemptRecord{}, errOveragePreferenceCorrupt
+ }
+ for number := int64(1); number < i.Attempt; number++ {
+ previous, err := loadSignupAttemptTx(tx, i, number)
+ if errors.Is(err, sql.ErrNoRows) {
+ return signupAttemptRecord{}, errOveragePreferenceCorrupt
+ }
+ if err != nil {
+ return signupAttemptRecord{}, err
+ }
+ if previous.State != "collision" {
+ return signupAttemptRecord{}, errOveragePreferenceCorrupt
+ }
+ }
+ current, err := loadSignupAttemptTx(tx, i, i.Attempt)
+ if errors.Is(err, sql.ErrNoRows) {
+ return signupAttemptRecord{}, errOveragePreferenceCorrupt
+ }
+ return current, err
+}
+
+func loadSignupAttemptTx(tx *sql.Tx, i signupIntent, number int64) (signupAttemptRecord, error) {
+ var r signupAttemptRecord
+ var b, o []byte
+ var account int64
+ err := tx.QueryRow(`SELECT tuple,state,observation,account_id FROM signup_mint_attempts WHERE intent_id=? AND number=?`, i.Header.ID, number).Scan(&b, &r.State, &o, &account)
+ if err != nil {
+ return r, err
+ }
+ if err = signupDecode(b, &r.Tuple); err != nil {
+ return r, err
+ }
+ a := r.Tuple
+ if a.IntentID != i.Header.ID || a.Number != number || a.AccountID != account || !signupIdentity(a.AccountID, a.Sqid) || a.Key != domains.AccountManifestKey(a.Sqid) || !signupNow(a.CreatedAt) || len(a.Body) == 0 || a.BodyDigest != signupDigest("body", a.Body) || a.BindingDigest != signupAttemptBinding(i, a) {
+ return r, errOveragePreferenceCorrupt
+ }
+ if len(o) > 0 {
+ r.Observation = &signupObservation{}
+ if err = signupDecode(o, r.Observation); err != nil {
+ return r, err
+ }
+ if r.State != "held" && classifySignupObservation(a, *r.Observation) != r.State {
+ return r, errOveragePreferenceCorrupt
+ }
+ }
+ if (r.State == "collision" || r.State == "observed_live" || r.State == "observed_retired") && r.Observation == nil {
+ return r, errOveragePreferenceCorrupt
+ }
+ return r, nil
+}
+func signupCandidateVacantTx(tx *sql.Tx, a signupAttempt) error {
+ var n int
+ err := tx.QueryRow(`SELECT (SELECT count(*) FROM accounts WHERE id=? OR sqid=?) + (SELECT count(*) FROM memberships WHERE account_id=?) + (SELECT count(*) FROM billing_retirements WHERE account_id=?)`, a.AccountID, a.Sqid, a.AccountID, a.AccountID).Scan(&n)
+ if err != nil {
+ return err
+ }
+ if n != 0 {
+ return errOveragePreferenceConflict
+ }
+ return nil
+}
+func (s *Store) recordSignupAttempt(raw, id string, scope []byte, a signupAttempt, now time.Time) (signupAttemptRecord, error) {
+ a.CreatedAt = a.CreatedAt.UTC()
+ if a.BodyDigest == "" {
+ a.BodyDigest = signupDigest("body", a.Body)
+ }
+ if a.BodyDigest != signupDigest("body", a.Body) {
+ return signupAttemptRecord{}, os.ErrInvalid
+ }
+ tx, err := s.signupTx()
+ if err != nil {
+ return signupAttemptRecord{}, err
+ }
+ defer tx.Rollback()
+ i, err := authorizedSignupTx(tx, raw, id, scope, now)
+ if err != nil {
+ return signupAttemptRecord{}, err
+ }
+ if i.Phase != "prepared" || a.IntentID != id || a.Number <= 0 || a.Number > signupMaxAttempts || !signupIdentity(a.AccountID, a.Sqid) || a.Key != domains.AccountManifestKey(a.Sqid) || !signupNow(a.CreatedAt) || len(a.Body) == 0 || len(a.Body) > 1<<20 || a.BindingDigest != signupAttemptBinding(i, a) {
+ return signupAttemptRecord{}, os.ErrInvalid
+ }
+ if a.Number <= i.Attempt {
+ r, e := loadSignupAttemptTx(tx, i, a.Number)
+ if e != nil {
+ return r, e
+ }
+ if !bytes.Equal(signupBytes(r.Tuple), signupBytes(a)) {
+ return signupAttemptRecord{}, errOveragePreferenceConflict
+ }
+ return r, nil
+ }
+ if a.Number != i.Attempt+1 {
+ return signupAttemptRecord{}, errOveragePreferenceConflict
+ }
+ if i.Attempt > 0 {
+ previous, e := loadSignupAttemptTx(tx, i, i.Attempt)
+ if e != nil {
+ return signupAttemptRecord{}, e
+ }
+ if previous.State != "collision" {
+ return signupAttemptRecord{}, errOveragePreferenceConflict
+ }
+ }
+ if err = signupCandidateVacantTx(tx, a); err != nil {
+ return signupAttemptRecord{}, err
+ }
+ _, err = tx.Exec(`INSERT INTO signup_mint_attempts(intent_id,number,account_id,tuple,state) VALUES(?,?,?,?,'prepared')`, id, a.Number, a.AccountID, signupBytes(a))
+ if err != nil {
+ return signupAttemptRecord{}, err
+ }
+ _, err = tx.Exec(`UPDATE signup_intents SET attempt=? WHERE id=? AND phase='prepared' AND attempt=?`, a.Number, id, i.Attempt)
+ if err != nil {
+ return signupAttemptRecord{}, err
+ }
+ return signupAttemptRecord{Tuple: a, State: "prepared"}, tx.Commit()
+}
+
+// Observation fields are supplied by a later, independently validated reader.
+// No observation can create a membership or advance the recorded candidate.
+func classifySignupObservation(a signupAttempt, o signupObservation) string {
+ if o.AccountID != a.AccountID || o.Sqid != a.Sqid || o.Key != a.Key || !signupNow(o.CreatedAt) || len(o.Body) == 0 {
+ return "held"
+ }
+ if o.IntentID == a.IntentID {
+ if o.Attempt != a.Number || o.BindingDigest != a.BindingDigest || !o.CreatedAt.Equal(a.CreatedAt) {
+ return "held"
+ }
+ if o.Retired {
+ return "observed_retired"
+ }
+ if !bytes.Equal(o.Body, a.Body) {
+ return "held"
+ }
+ return "observed_live"
+ }
+ if o.IntentID != "" {
+ if !overageDigest.MatchString(o.IntentID) || o.Attempt <= 0 || !overageDigest.MatchString(o.BindingDigest) {
+ return "held"
+ }
+ return "collision"
+ }
+ if o.Attempt != 0 || o.BindingDigest != "" {
+ return "held"
+ }
+ if !o.CreatedAt.Equal(a.CreatedAt) {
+ return "collision"
+ }
+ return "held"
+}
+func signupObservationClaimsCandidate(a signupAttempt, o signupObservation) bool {
+ return o.IntentID == a.IntentID ||
+ (o.IntentID == "" && o.Attempt == 0 && o.BindingDigest == "" && o.AccountID == a.AccountID && o.Sqid == a.Sqid && o.Key == a.Key && o.CreatedAt.Equal(a.CreatedAt))
+}
+
+func (s *Store) observeSignupAttempt(id string, scope []byte, number int64, o signupObservation) (signupAttemptRecord, error) {
+ o.CreatedAt = o.CreatedAt.UTC()
+ if len(o.Body) > 1<<20 || len(o.ETag) > 4096 {
+ return signupAttemptRecord{}, os.ErrInvalid
+ }
+ tx, err := s.signupTx()
+ if err != nil {
+ return signupAttemptRecord{}, err
+ }
+ defer tx.Rollback()
+ i, err := loadSignupTx(tx, id)
+ if err != nil {
+ return signupAttemptRecord{}, err
+ }
+ if !bytes.Equal(scope, i.Header.Scope) {
+ return signupAttemptRecord{}, errOveragePreferenceDenied
+ }
+ r, err := loadSignupAttemptTx(tx, i, number)
+ if err != nil {
+ return r, err
+ }
+ state := classifySignupObservation(r.Tuple, o)
+ // Own or ambiguous-own evidence survives a later identity rewrite.
+ // Keep that observation while held so later retries cannot forget it.
+ if r.Observation != nil && signupObservationClaimsCandidate(r.Tuple, *r.Observation) && state != "observed_live" && state != "observed_retired" {
+ state = "held"
+ o = *r.Observation
+ }
+
+ if r.State == "collision" {
+ old := r.Observation
+ if state != "collision" || old == nil || old.AccountID != o.AccountID || old.Sqid != o.Sqid || old.Key != o.Key || old.IntentID != o.IntentID || old.Attempt != o.Attempt || old.BindingDigest != o.BindingDigest || !old.CreatedAt.Equal(o.CreatedAt) {
+ return signupAttemptRecord{}, errOveragePreferenceConflict
+ }
+ return r, nil
+ }
+ if r.State == "observed_retired" || i.Phase == "completed" || i.Phase == "retired" {
+ if r.State != state || !bytes.Equal(signupBytes(r.Observation), signupBytes(o)) {
+ return signupAttemptRecord{}, errOveragePreferenceConflict
+ }
+ return r, nil
+ }
+ _, err = tx.Exec(`UPDATE signup_mint_attempts SET state=?,observation=? WHERE intent_id=? AND number=?`, state, signupBytes(o), id, number)
+ if err != nil {
+ return signupAttemptRecord{}, err
+ }
+ if state == "observed_retired" && i.Phase == "prepared" {
+ _, err = tx.Exec(`UPDATE signup_intents SET phase='retired' WHERE id=? AND phase='prepared'`, id)
+ if err != nil {
+ return signupAttemptRecord{}, err
+ }
+ }
+ r.State = state
+ r.Observation = &o
+ return r, tx.Commit()
+}
+
+func loadSignupBirthTx(tx *sql.Tx, i signupIntent) (signupBirthResult, error) {
+ var out signupBirthResult
+ var b, scope []byte
+ var digest, sqid string
+ var account int64
+ err := tx.QueryRow(`SELECT receipt,digest,scope,account_id,sqid FROM account_birth_receipts WHERE intent_id=?`, i.Header.ID).Scan(&b, &digest, &scope, &account, &sqid)
+ if errors.Is(err, sql.ErrNoRows) {
+ return out, errOveragePreferenceCorrupt
+ }
+ if err != nil {
+ return out, err
+ }
+ if err = signupDecode(b, &out.Historical); err != nil {
+ return out, err
+ }
+ h := out.Historical
+ if digest != signupDigest("receipt", b) || h.IntentID != i.Header.ID || h.IntentDigest != i.Digest || !bytes.Equal(scope, i.Header.Scope) || h.ScopeDigest != signupDigest("scope", scope) || h.AccountID != account || h.Sqid != sqid || h.Name != i.Header.Name || h.Owner != i.Header.Subject || h.Attempt != i.Attempt || !signupNow(h.CommittedAt) {
+ return out, errOveragePreferenceCorrupt
+ }
+ a, err := loadSignupCurrentAttemptTx(tx, i)
+ if err != nil {
+ return out, err
+ }
+ if a.State != "observed_live" || h.BindingDigest != a.Tuple.BindingDigest || account != a.Tuple.AccountID || !h.CreatedAt.Equal(a.Tuple.CreatedAt) {
+ return out, errOveragePreferenceCorrupt
+ }
+ if err = authorizeOverageOwnerTx(tx, account, i.Header.Subject); err != nil {
+ return signupBirthResult{}, err
+ }
+ var currentCreated int64
+ err = tx.QueryRow(`SELECT id,sqid,name,created_at FROM accounts WHERE id=? AND deleted_at=0`, account).Scan(&out.Current.ID, &out.Current.Sqid, &out.Current.Name, &currentCreated)
+ if err != nil {
+ return signupBirthResult{}, err
+ }
+ out.Current.CreatedAt = time.Unix(currentCreated, 0).UTC()
+ out.Current.Role = RoleOwner
+ if out.Current.Sqid != h.Sqid {
+ return signupBirthResult{}, errOveragePreferenceCorrupt
+ }
+ out.Preference, err = loadOveragePreferenceTx(tx, account)
+ if err != nil {
+ return signupBirthResult{}, err
+ }
+ if h.Outcome == "on" {
+ if i.Seal == nil || !i.Seal.RequestedOn || h.Preference == nil || h.PreferenceDigest == "" {
+ return signupBirthResult{}, errOveragePreferenceCorrupt
+ }
+ p, d, e := scanOverageReceipt(tx.QueryRow(`SELECT operation_id,account_id,actor,request_digest,revision,requested_on,terms_version,price_book,currency,disclosure_digest,offer_digest,changed_at FROM overage_preference_receipts WHERE operation_id=?`, i.Header.PreferenceOperationID))
+ if e != nil {
+ return signupBirthResult{}, errOveragePreferenceCorrupt
+ }
+ if d != h.PreferenceDigest || !bytes.Equal(signupBytes(p), signupBytes(h.Preference)) || p.AccountID != account || p.Actor != i.Header.Subject || p.Revision != 1 || !p.RequestedOn || p.Evidence != i.Offer.Evidence {
+ return signupBirthResult{}, errOveragePreferenceCorrupt
+ }
+ } else {
+ if h.Preference != nil || h.PreferenceDigest != "" || (h.Outcome != "off" && h.Outcome != "choice_not_applied") || (h.Outcome == "off" && i.Seal.RequestedOn) || (h.Outcome == "choice_not_applied" && !i.Seal.RequestedOn) {
+ return signupBirthResult{}, errOveragePreferenceCorrupt
+ }
+ }
+ return out, nil
+}
+
+// applyChoice is a caller decision after offer/capability validation. It is not
+// derived from stored bytes. Expiry additionally prevents applying an old ON.
+func (s *Store) completeSignup(raw, id string, scope []byte, intentDigest string, applyChoice bool, now time.Time) (signupBirthResult, error) {
+ tx, err := s.signupTx()
+ if err != nil {
+ return signupBirthResult{}, err
+ }
+ defer tx.Rollback()
+ i, err := authorizedSignupTx(tx, raw, id, scope, now)
+ if err != nil {
+ return signupBirthResult{}, err
+ }
+ if i.Digest != intentDigest {
+ return signupBirthResult{}, errOveragePreferenceConflict
+ }
+ if i.Phase == "completed" {
+ return loadSignupBirthTx(tx, i)
+ }
+ if i.Phase != "prepared" || i.Attempt == 0 {
+ return signupBirthResult{}, errOveragePreferenceConflict
+ }
+ a, err := loadSignupCurrentAttemptTx(tx, i)
+ if err != nil {
+ return signupBirthResult{}, err
+ }
+ if a.State != "observed_live" {
+ return signupBirthResult{}, errOveragePreferenceConflict
+ }
+ if err = signupCandidateVacantTx(tx, a.Tuple); err != nil {
+ return signupBirthResult{}, err
+ }
+ current, err := loadOveragePreferenceTx(tx, a.Tuple.AccountID)
+ if err != nil {
+ return signupBirthResult{}, err
+ }
+ if current.Revision != 0 {
+ return signupBirthResult{}, errOveragePreferenceConflict
+ }
+ if applyChoice && !i.Seal.RequestedOn {
+ return signupBirthResult{}, os.ErrInvalid
+ }
+ if err = createAccountTx(tx, a.Tuple.AccountID, a.Tuple.Sqid, i.Header.Name, i.Header.Subject, a.Tuple.CreatedAt); err != nil {
+ return signupBirthResult{}, err
+ }
+ h := signupBirth{IntentID: id, IntentDigest: i.Digest, BindingDigest: a.Tuple.BindingDigest, ScopeDigest: signupDigest("scope", scope), Attempt: a.Tuple.Number, AccountID: a.Tuple.AccountID, Sqid: a.Tuple.Sqid, Name: i.Header.Name, Owner: i.Header.Subject, Outcome: "off", CreatedAt: a.Tuple.CreatedAt, CommittedAt: now.UTC()}
+ if i.Seal.RequestedOn {
+ h.Outcome = "choice_not_applied"
+ if applyChoice && now.Before(i.Offer.ExpiresAt) {
+ if _, err = signupFreshTx(tx, raw, i.Header.Subject, now); err != nil {
+ return signupBirthResult{}, err
+ }
+ c := overagePreferenceChange{AccountID: h.AccountID, Actor: h.Owner, OperationID: i.Header.PreferenceOperationID, RequestedOn: true, Evidence: i.Offer.Evidence}
+ if err = validateOverageChange(c); err != nil {
+ return signupBirthResult{}, err
+ }
+ if err = authorizeOverageOwnerTx(tx, h.AccountID, h.Owner); err != nil {
+ return signupBirthResult{}, err
+ }
+ // A birth requires a new operation, never an existing receipt replay.
+ var n int
+ if err = tx.QueryRow(`SELECT count(*) FROM overage_preference_receipts WHERE operation_id=?`, c.OperationID).Scan(&n); err != nil {
+ return signupBirthResult{}, err
+ }
+ if n != 0 {
+ return signupBirthResult{}, errOveragePreferenceConflict
+ }
+ p, e := changeOveragePreferenceTx(tx, c, now)
+ if e != nil {
+ return signupBirthResult{}, e
+ }
+ h.Preference = &p
+ h.PreferenceDigest = overageChangeDigest(c)
+ h.Outcome = "on"
+ }
+ }
+ b := signupBytes(h)
+ _, err = tx.Exec(`INSERT INTO account_birth_receipts(intent_id,scope,account_id,sqid,receipt,digest) VALUES(?,?,?,?,?,?)`, id, scope, h.AccountID, h.Sqid, b, signupDigest("receipt", b))
+ if err != nil {
+ return signupBirthResult{}, err
+ }
+ _, err = tx.Exec(`UPDATE signup_intents SET phase='completed' WHERE id=? AND phase='prepared'`, id)
+ if err != nil {
+ return signupBirthResult{}, err
+ }
+ i.Phase = "completed"
+ out, err := loadSignupBirthTx(tx, i)
+ if err != nil {
+ return signupBirthResult{}, err
+ }
+ if err = tx.Commit(); err != nil {
+ return signupBirthResult{}, err
+ }
+ // Last-account navigation is not part of birth authority or its receipt.
+ _ = s.SetLastAccount(i.Header.Subject, h.AccountID)
+ return out, nil
+}
diff --git a/internal/console/signuprecords_test.go b/internal/console/signuprecords_test.go
new file mode 100644
index 000000000..9664454bc
--- /dev/null
+++ b/internal/console/signuprecords_test.go
@@ -0,0 +1,1050 @@
+package console
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "path/filepath"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/domains"
+)
+
+type signupFixture struct {
+ s *Store
+ raw string
+ now time.Time
+ scope []byte
+ i signupIntent
+ a signupAttempt
+}
+
+func newSignupFixture(t *testing.T, on bool) signupFixture {
+ t.Helper()
+ s := newOverageStore(t, filepath.Join(t.TempDir(), "db"))
+ now := time.Date(2026, 10, 4, 5, 0, 0, 123, time.UTC)
+ raw, e := s.CreateSession(overageOwner, "", now)
+ if e != nil {
+ t.Fatal(e)
+ }
+ if e = s.RecordStepUp(raw, overageOwner, now.Add(-time.Second), now); e != nil {
+ t.Fatal(e)
+ }
+ f := signupFixture{s: s, raw: raw, now: now, scope: []byte("opaque-reviewed-scope")}
+ f.i, e = s.createSignupDraft(raw, overageOperation(1), "original", f.scope, now)
+ if e != nil {
+ t.Fatal(e)
+ }
+ if on {
+ f.i, e = s.freezeSignupOffer(raw, f.i.Header.ID, f.scope, signupOffer{Payload: []byte("bound caller offer:" + f.i.Header.ID), Evidence: overageEvidenceForTest(), ExpiresAt: now.Add(time.Hour)}, now)
+ if e != nil {
+ t.Fatal(e)
+ }
+ }
+ return f
+}
+func (f *signupFixture) seal(t *testing.T, on bool) {
+ t.Helper()
+ var b []byte
+ if f.i.Offer != nil {
+ b = signupBytes(f.i.Offer)
+ }
+ var e error
+ f.i, e = f.s.sealSignup(f.raw, f.i.Header.ID, f.scope, true, on, signupDigest("offer", b), f.now)
+ if e != nil {
+ t.Fatal(e)
+ }
+}
+func (f *signupFixture) candidate(t *testing.T) {
+ t.Helper()
+ f.a = signupAttempt{IntentID: f.i.Header.ID, Number: 1, AccountID: 42, Sqid: domains.Sqid(42), CreatedAt: f.now, Body: []byte("exact frozen body")}
+ f.a.Key = domains.AccountManifestKey(f.a.Sqid)
+ f.a.BindingDigest = signupAttemptBinding(f.i, f.a)
+ if _, e := f.s.recordSignupAttempt(f.raw, f.i.Header.ID, f.scope, f.a, f.now); e != nil {
+ t.Fatal(e)
+ }
+}
+func (f *signupFixture) live(t *testing.T) {
+ t.Helper()
+ o := signupObservation{AccountID: f.a.AccountID, Sqid: f.a.Sqid, Key: f.a.Key, IntentID: f.i.Header.ID, BindingDigest: f.a.BindingDigest, Attempt: f.a.Number, CreatedAt: f.a.CreatedAt, Body: f.a.Body, ETag: "etag"}
+ if _, e := f.s.observeSignupAttempt(f.i.Header.ID, f.scope, f.a.Number, o); e != nil {
+ t.Fatal(e)
+ }
+}
+func (f *signupFixture) complete(on bool) (signupBirthResult, error) {
+ return f.s.completeSignup(f.raw, f.i.Header.ID, f.scope, f.i.Digest, on, f.now)
+}
+func signupCount(t *testing.T, s *Store, q string, args ...any) int {
+ t.Helper()
+ var n int
+ if e := s.db.QueryRow(q, args...).Scan(&n); e != nil {
+ t.Fatal(e)
+ }
+ return n
+}
+
+func TestSignupDraftSealReplayAndSnapshot(t *testing.T) {
+ f := newSignupFixture(t, true)
+ original := f.i
+ retry, e := f.s.createSignupDraft(f.raw, overageOperation(1), "original", f.scope, f.now.Add(time.Second))
+ if e != nil || !bytes.Equal(signupBytes(original), signupBytes(retry)) {
+ t.Fatalf("draft replay changed: %v", e)
+ }
+ if _, e = f.s.createSignupDraft(f.raw, overageOperation(1), "changed", f.scope, f.now); !errors.Is(e, errOveragePreferenceConflict) {
+ t.Fatalf("changed draft: %v", e)
+ }
+ altered := *f.i.Offer
+ altered.Payload = []byte("changed")
+ if _, e = f.s.freezeSignupOffer(f.raw, f.i.Header.ID, f.scope, altered, f.now); !errors.Is(e, errOveragePreferenceConflict) {
+ t.Fatalf("offer overwrite: %v", e)
+ }
+ f.seal(t, true)
+ sealed := signupBytes(f.i)
+ if e = f.s.ClearStepUp(f.raw); e != nil {
+ t.Fatal(e)
+ }
+ if e = f.s.RecordStepUp(f.raw, overageOwner, f.now.Add(time.Hour), f.now.Add(time.Hour)); e != nil {
+ t.Fatal(e)
+ }
+ retry, e = f.s.sealSignup(f.raw, f.i.Header.ID, f.scope, true, true, f.i.Seal.OfferDigest, f.now.Add(2*time.Hour))
+ if e != nil || !bytes.Equal(sealed, signupBytes(retry)) {
+ t.Fatalf("seal recovery after expiry changed: %v", e)
+ }
+ if bytes.Contains(sealed, []byte(f.raw)) || bytes.Contains(sealed, []byte(hashToken(f.raw))) {
+ t.Fatal("session evidence persisted")
+ }
+ if _, e = f.s.sealSignup(f.raw, f.i.Header.ID, f.scope, true, false, f.i.Seal.OfferDigest, f.now); !errors.Is(e, errOveragePreferenceConflict) {
+ t.Fatalf("changed seal: %v", e)
+ }
+}
+func TestSignupFreshnessAndCurrentSession(t *testing.T) {
+ for _, kind := range []string{"no-step-up", "wrong-address", "stale", "no-session", "expired-session"} {
+ t.Run(kind, func(t *testing.T) {
+ f := newSignupFixture(t, false)
+ switch kind {
+ case "no-step-up":
+ f.s.ClearStepUp(f.raw)
+ case "wrong-address":
+ f.s.RecordStepUp(f.raw, "other@example.test", f.now, f.now)
+ case "stale":
+ f.s.RecordStepUp(f.raw, overageOwner, f.now.Add(-StepUpWindow-time.Second), f.now)
+ case "no-session":
+ f.s.DeleteSession(f.raw)
+ case "expired-session":
+ _, e := f.s.db.Exec(`UPDATE sessions SET expires_at=?`, f.now.Add(-time.Second).Unix())
+ if e != nil {
+ t.Fatal(e)
+ }
+ }
+ if _, e := f.s.sealSignup(f.raw, f.i.Header.ID, f.scope, true, false, signupDigest("offer", nil), f.now); !errors.Is(e, errOveragePreferenceDenied) {
+ t.Fatalf("authority %s: %v", kind, e)
+ }
+ if signupCount(t, f.s, `SELECT count(*) FROM signup_intents WHERE phase='draft'`) != 1 {
+ t.Fatal("refusal changed draft")
+ }
+ })
+ }
+}
+func TestSignupBirthAtomicRollback(t *testing.T) {
+ for _, table := range []string{"accounts", "memberships", "overage_preferences", "overage_preference_receipts", "account_birth_receipts", "signup_intents"} {
+ t.Run(table, func(t *testing.T) {
+ f := newSignupFixture(t, true)
+ f.seal(t, true)
+ f.candidate(t)
+ f.live(t)
+ verb := "INSERT"
+ when := ""
+ if table == "signup_intents" {
+ verb = "UPDATE"
+ when = " WHEN NEW.phase='completed'"
+ }
+ if _, e := f.s.db.Exec(fmt.Sprintf(`CREATE TRIGGER test_fail BEFORE %s ON %s%s BEGIN SELECT RAISE(ABORT,'injected'); END`, verb, table, when)); e != nil {
+ t.Fatal(e)
+ }
+ if _, e := f.complete(true); e == nil {
+ t.Fatal("injected failure accepted")
+ }
+ for _, q := range []string{`SELECT count(*) FROM accounts WHERE id=42`, `SELECT count(*) FROM memberships WHERE account_id=42`, `SELECT count(*) FROM overage_preferences WHERE account_id=42`, `SELECT count(*) FROM overage_preference_receipts WHERE account_id=42`, `SELECT count(*) FROM account_birth_receipts`} {
+ if signupCount(t, f.s, q) != 0 {
+ t.Fatalf("partial commit: %s", q)
+ }
+ }
+ if signupCount(t, f.s, `SELECT count(*) FROM signup_intents WHERE phase='prepared'`) != 1 {
+ t.Fatal("intent completed despite rollback")
+ }
+ if _, e := f.s.db.Exec(`DROP TRIGGER test_fail`); e != nil {
+ t.Fatal(e)
+ }
+ out, e := f.complete(true)
+ if e != nil || out.Historical.Outcome != "on" {
+ t.Fatalf("retry after rollback: %+v %v", out, e)
+ }
+ })
+ }
+}
+func TestSignupBirthReplayAfterOFFRenameOwnerLoss(t *testing.T) {
+ f := newSignupFixture(t, true)
+ f.seal(t, true)
+ f.candidate(t)
+ f.live(t)
+ first, e := f.complete(true)
+ if e != nil {
+ t.Fatal(e)
+ }
+ if first.Preference.Revision != 1 || !first.Preference.RequestedOn {
+ t.Fatalf("initial preference: %+v", first)
+ }
+ if _, e = f.s.changeOveragePreference(overageChange(42, overageOwner, 1, false, 2), f.now.Add(time.Second)); e != nil {
+ t.Fatal(e)
+ }
+ if _, e = f.s.db.Exec(`UPDATE accounts SET name='Renamed' WHERE id=42`); e != nil {
+ t.Fatal(e)
+ }
+ again, e := f.complete(true)
+ if e != nil {
+ t.Fatal(e)
+ }
+ if !bytes.Equal(signupBytes(first.Historical), signupBytes(again.Historical)) || again.Current.Name != "Renamed" || again.Preference.RequestedOn || again.Preference.Revision != 2 {
+ t.Fatalf("historical/current mixed: %+v", again)
+ }
+ if signupCount(t, f.s, `SELECT count(*) FROM overage_preference_receipts WHERE account_id=42`) != 2 {
+ t.Fatal("replay mutated history")
+ }
+ if e = f.s.AddMember(42, "new@example.test", RoleOwner, f.now); e != nil {
+ t.Fatal(e)
+ }
+ if e = f.s.RemoveMember(42, overageOwner); e != nil {
+ t.Fatal(e)
+ }
+ if out, e := f.complete(true); !errors.Is(e, errOveragePreferenceDenied) || out.Historical.IntentID != "" {
+ t.Fatalf("former owner disclosure: %+v %v", out, e)
+ }
+ other, e := f.s.CreateSession("new@example.test", "", f.now)
+ if e != nil {
+ t.Fatal(e)
+ }
+ if _, e = f.s.completeSignup(other, f.i.Header.ID, f.scope, f.i.Digest, true, f.now); !errors.Is(e, errOveragePreferenceDenied) {
+ t.Fatalf("new owner inherited intent: %v", e)
+ }
+}
+func TestSignupExpiredOrInvalidatedChoiceStaysOff(t *testing.T) {
+ for _, expired := range []bool{false, true} {
+ t.Run(fmt.Sprint(expired), func(t *testing.T) {
+ f := newSignupFixture(t, true)
+ f.seal(t, true)
+ f.candidate(t)
+ f.live(t)
+ if expired {
+ f.now = f.now.Add(2 * time.Hour)
+ }
+ out, e := f.complete(expired)
+ if e != nil {
+ t.Fatal(e)
+ }
+ if out.Historical.Outcome != "choice_not_applied" || out.Preference.Revision != 0 || !f.i.Seal.RequestedOn {
+ t.Fatalf("invalid choice applied: %+v", out)
+ }
+ })
+ }
+ f := newSignupFixture(t, false)
+ f.seal(t, false)
+ f.candidate(t)
+ f.live(t)
+ out, e := f.complete(false)
+ if e != nil || out.Historical.Outcome != "off" || out.Preference.Revision != 0 {
+ t.Fatalf("default off: %+v %v", out, e)
+ }
+}
+func TestSignupConcurrentSealsAndAttachments(t *testing.T) {
+ f := newSignupFixture(t, true)
+ start := make(chan struct{})
+ errs := make(chan error, 2)
+ for _, on := range []bool{false, true} {
+ go func(on bool) {
+ <-start
+ _, e := f.s.sealSignup(f.raw, f.i.Header.ID, f.scope, true, on, signupDigest("offer", signupBytes(f.i.Offer)), f.now)
+ errs <- e
+ }(on)
+ }
+ close(start)
+ successes, conflicts := 0, 0
+ for k := 0; k < 2; k++ {
+ e := <-errs
+ if e == nil {
+ successes++
+ } else if errors.Is(e, errOveragePreferenceConflict) {
+ conflicts++
+ } else {
+ t.Fatal(e)
+ }
+ }
+ if successes != 1 || conflicts != 1 {
+ t.Fatalf("seal winners %d conflicts %d", successes, conflicts)
+ }
+ f = newSignupFixture(t, false)
+ start = make(chan struct{})
+ errs = make(chan error, 2)
+ for _, payload := range []string{"one", "two"} {
+ go func(p string) {
+ <-start
+ _, e := f.s.freezeSignupOffer(f.raw, f.i.Header.ID, f.scope, signupOffer{Payload: []byte(p), Evidence: overageEvidenceForTest(), ExpiresAt: f.now.Add(time.Hour)}, f.now)
+ errs <- e
+ }(payload)
+ }
+ close(start)
+ successes, conflicts = 0, 0
+ for k := 0; k < 2; k++ {
+ e := <-errs
+ if e == nil {
+ successes++
+ } else if errors.Is(e, errOveragePreferenceConflict) {
+ conflicts++
+ } else {
+ t.Fatal(e)
+ }
+ }
+ if successes != 1 || conflicts != 1 {
+ t.Fatalf("offer winners %d conflicts %d", successes, conflicts)
+ }
+}
+func TestSignupSealAttachmentOrdering(t *testing.T) {
+ for _, sealFirst := range []bool{false, true} {
+ t.Run(fmt.Sprint(sealFirst), func(t *testing.T) {
+ f := newSignupFixture(t, false)
+ o := signupOffer{Payload: []byte("offer"), Evidence: overageEvidenceForTest(), ExpiresAt: f.now.Add(time.Hour)}
+ if sealFirst {
+ f.seal(t, false)
+ if _, e := f.s.freezeSignupOffer(f.raw, f.i.Header.ID, f.scope, o, f.now); !errors.Is(e, errOveragePreferenceConflict) {
+ t.Fatalf("offer attached after seal: %v", e)
+ }
+ } else {
+ if _, e := f.s.freezeSignupOffer(f.raw, f.i.Header.ID, f.scope, o, f.now); e != nil {
+ t.Fatal(e)
+ }
+ if _, e := f.s.sealSignup(f.raw, f.i.Header.ID, f.scope, true, false, signupDigest("offer", nil), f.now); !errors.Is(e, errOveragePreferenceConflict) {
+ t.Fatalf("seal ignored newly frozen offer: %v", e)
+ }
+ }
+ })
+ }
+}
+func TestSignupCancellationAndRetiredTerminal(t *testing.T) {
+ f := newSignupFixture(t, false)
+ if e := f.s.cancelSignup(f.raw, f.i.Header.ID, f.scope, f.now); e != nil {
+ t.Fatal(e)
+ }
+ if _, e := f.s.sealSignup(f.raw, f.i.Header.ID, f.scope, true, false, signupDigest("offer", nil), f.now); !errors.Is(e, errOveragePreferenceConflict) {
+ t.Fatalf("cancelled draft sealed: %v", e)
+ }
+ f = newSignupFixture(t, true)
+ f.seal(t, true)
+ f.candidate(t)
+ f.live(t)
+ if e := f.s.cancelSignup(f.raw, f.i.Header.ID, f.scope, f.now); e != nil {
+ t.Fatal(e)
+ }
+ if _, e := f.complete(true); !errors.Is(e, errOveragePreferenceConflict) {
+ t.Fatalf("cancelled birth: %v", e)
+ }
+ if signupCount(t, f.s, `SELECT count(*) FROM signup_mint_attempts`) != 1 {
+ t.Fatal("cancellation discarded attempt")
+ }
+ f = newSignupFixture(t, false)
+ f.seal(t, false)
+ f.candidate(t)
+ f.live(t)
+ o := signupObservation{AccountID: 42, Sqid: f.a.Sqid, Key: f.a.Key, IntentID: f.i.Header.ID, Attempt: 1, BindingDigest: f.a.BindingDigest, CreatedAt: f.now, Body: []byte("tombstone"), Retired: true}
+ if _, e := f.s.observeSignupAttempt(f.i.Header.ID, f.scope, 1, o); e != nil {
+ t.Fatal(e)
+ }
+ if signupCount(t, f.s, `SELECT count(*) FROM signup_intents WHERE phase='retired'`) != 1 {
+ t.Fatal("retirement not terminal")
+ }
+ if _, e := f.complete(false); e == nil {
+ t.Fatal("retired birth accepted")
+ }
+ if _, e := f.s.db.Exec(`UPDATE signup_intents SET phase='prepared'`); e == nil {
+ t.Fatal("retirement reversed")
+ }
+}
+func TestSignupCandidateExclusionAndCollision(t *testing.T) {
+ for _, kind := range []string{"account", "membership", "retirement"} {
+ t.Run(kind, func(t *testing.T) {
+ f := newSignupFixture(t, false)
+ f.seal(t, false)
+ switch kind {
+ case "account":
+ if e := f.s.CreateAccount(42, domains.Sqid(42), "Existing", overageOwner, f.now); e != nil {
+ t.Fatal(e)
+ }
+ case "membership":
+ if _, e := f.s.db.Exec(`INSERT INTO memberships(account_id,address,role,added_at) VALUES(42,?,'owner',1)`, overageOwner); e != nil {
+ t.Fatal(e)
+ }
+ case "retirement":
+ if _, e := f.s.db.Exec(`INSERT INTO billing_retirements(account_id,started_at) VALUES(42,1)`); e != nil {
+ t.Fatal(e)
+ }
+ }
+ a := signupAttempt{IntentID: f.i.Header.ID, Number: 1, AccountID: 42, Sqid: domains.Sqid(42), Key: domains.AccountManifestKey(domains.Sqid(42)), CreatedAt: f.now, Body: []byte("body")}
+ a.BindingDigest = signupAttemptBinding(f.i, a)
+ if _, e := f.s.recordSignupAttempt(f.raw, f.i.Header.ID, f.scope, a, f.now); !errors.Is(e, errOveragePreferenceConflict) {
+ t.Fatalf("local %s reused: %v", kind, e)
+ }
+ })
+ }
+ f := newSignupFixture(t, false)
+ f.seal(t, false)
+ f.candidate(t)
+ next := f.a
+ next.Number = 2
+ next.AccountID = 43
+ next.Sqid = domains.Sqid(43)
+ next.Key = domains.AccountManifestKey(next.Sqid)
+ next.BindingDigest = signupAttemptBinding(f.i, next)
+ if _, e := f.s.recordSignupAttempt(f.raw, f.i.Header.ID, f.scope, next, f.now); !errors.Is(e, errOveragePreferenceConflict) {
+ t.Fatalf("unresolved advance: %v", e)
+ }
+ o := signupObservation{AccountID: 42, Sqid: f.a.Sqid, Key: f.a.Key, CreatedAt: time.Time{}, Body: []byte("unmarked")}
+ r, e := f.s.observeSignupAttempt(f.i.Header.ID, f.scope, 1, o)
+ if e != nil || r.State != "held" {
+ t.Fatalf("inconclusive observation: %+v %v", r, e)
+ }
+ o.CreatedAt = f.now.Add(-time.Second)
+ r, e = f.s.observeSignupAttempt(f.i.Header.ID, f.scope, 1, o)
+ if e != nil || r.State != "collision" {
+ t.Fatalf("foreign winner: %+v %v", r, e)
+ }
+ if _, e = f.s.recordSignupAttempt(f.raw, f.i.Header.ID, f.scope, next, f.now); e != nil {
+ t.Fatal(e)
+ }
+ if _, e = f.s.observeSignupAttempt(f.i.Header.ID, f.scope, 1, signupObservation{}); !errors.Is(e, errOveragePreferenceConflict) {
+ t.Fatalf("terminal collision altered: %v", e)
+ }
+}
+func TestSignupImmutability(t *testing.T) {
+ f := newSignupFixture(t, true)
+ f.seal(t, true)
+ f.candidate(t)
+ f.live(t)
+ if _, e := f.complete(true); e != nil {
+ t.Fatal(e)
+ }
+ queries := []string{
+ `UPDATE signup_intents SET header=X'00'`, `UPDATE signup_intents SET offer=X'00'`, `UPDATE signup_intents SET seal=X'00'`, `DELETE FROM signup_intents`,
+ `INSERT OR REPLACE INTO signup_intents SELECT * FROM signup_intents`,
+ `UPDATE signup_mint_attempts SET tuple=X'00'`, `DELETE FROM signup_mint_attempts`, `INSERT OR REPLACE INTO signup_mint_attempts SELECT * FROM signup_mint_attempts`,
+ `UPDATE account_birth_receipts SET receipt=X'00'`, `DELETE FROM account_birth_receipts`, `INSERT OR REPLACE INTO account_birth_receipts SELECT * FROM account_birth_receipts`,
+ `UPDATE signup_intents SET phase='retired'`, `UPDATE signup_intents SET phase='prepared'`,
+ }
+ for _, q := range queries {
+ if _, e := f.s.db.Exec(q); e == nil {
+ t.Fatalf("immutable write accepted: %s", q)
+ }
+ }
+ if _, e := f.complete(true); e != nil {
+ t.Fatalf("refused writes damaged receipt: %v", e)
+ }
+}
+func TestSignupReplayCorruptionRefuses(t *testing.T) {
+ for _, kind := range []string{"birth-missing", "birth-malformed", "current-missing", "receipt-missing", "current-malformed"} {
+ t.Run(kind, func(t *testing.T) {
+ f := newSignupFixture(t, true)
+ f.seal(t, true)
+ f.candidate(t)
+ f.live(t)
+ if _, e := f.complete(true); e != nil {
+ t.Fatal(e)
+ }
+ var qs []string
+ switch kind {
+ case "birth-missing":
+ qs = []string{`DROP TRIGGER signup_birth_no_delete`, `DELETE FROM account_birth_receipts`}
+ case "birth-malformed":
+ qs = []string{`DROP TRIGGER signup_birth_no_update`, `UPDATE account_birth_receipts SET receipt=X'00'`}
+ case "current-missing":
+ qs = []string{`DELETE FROM overage_preferences`}
+ case "receipt-missing":
+ qs = []string{`DROP TRIGGER overage_preference_receipts_no_delete`, `DELETE FROM overage_preference_receipts`}
+ case "current-malformed":
+ qs = []string{`UPDATE overage_preferences SET revision=9`}
+ }
+ for _, q := range qs {
+ if _, e := f.s.db.Exec(q); e != nil {
+ t.Fatal(e)
+ }
+ }
+ if _, e := f.complete(true); e == nil {
+ t.Fatal("corrupt replay accepted")
+ }
+ })
+ }
+}
+func TestSignupGlobalOperationCollisionRollback(t *testing.T) {
+ f := newSignupFixture(t, true)
+ f.seal(t, true)
+ f.candidate(t)
+ f.live(t)
+ seedOverageAccount(t, f.s, 90, overageOwner, f.now)
+ c := overageChange(90, overageOwner, 0, false, 2)
+ c.OperationID = f.i.Header.PreferenceOperationID
+ if _, e := f.s.changeOveragePreference(c, f.now); e != nil {
+ t.Fatal(e)
+ }
+ if _, e := f.complete(true); !errors.Is(e, errOveragePreferenceConflict) {
+ t.Fatalf("operation collision: %v", e)
+ }
+ if signupCount(t, f.s, `SELECT count(*) FROM accounts WHERE id=42`) != 0 {
+ t.Fatal("collision committed birth")
+ }
+}
+func TestSignupConcurrentBirthAndOFF(t *testing.T) {
+ f := newSignupFixture(t, true)
+ f.seal(t, true)
+ f.candidate(t)
+ f.live(t)
+ var wg sync.WaitGroup
+ errs := make(chan error, 2)
+ start := make(chan struct{})
+ for k := 0; k < 2; k++ {
+ wg.Add(1)
+ go func() { defer wg.Done(); <-start; _, e := f.complete(true); errs <- e }()
+ }
+ close(start)
+ wg.Wait()
+ close(errs)
+ for e := range errs {
+ if e != nil {
+ t.Fatal(e)
+ }
+ }
+ if signupCount(t, f.s, `SELECT count(*) FROM account_birth_receipts`) != 1 || signupCount(t, f.s, `SELECT count(*) FROM overage_preference_receipts WHERE account_id=42`) != 1 {
+ t.Fatal("multiple birth commits")
+ }
+ errs = make(chan error, 2)
+ start = make(chan struct{})
+ for _, op := range []byte{3, 4} {
+ go func(op byte) {
+ <-start
+ _, e := f.s.changeOveragePreference(overageChange(42, overageOwner, 1, false, op), f.now.Add(time.Second))
+ errs <- e
+ }(op)
+ }
+ close(start)
+ win, stale := 0, 0
+ for k := 0; k < 2; k++ {
+ e := <-errs
+ if e == nil {
+ win++
+ } else if errors.Is(e, errOveragePreferenceStale) {
+ stale++
+ } else {
+ t.Fatal(e)
+ }
+ }
+ if win != 1 || stale != 1 {
+ t.Fatalf("revision ownership: %d %d", win, stale)
+ }
+ out, e := f.complete(true)
+ if e != nil || out.Preference.RequestedOn || out.Preference.Revision != 2 {
+ t.Fatalf("replay overwrote concurrent OFF: %+v %v", out, e)
+ }
+}
+func TestSignupSameIntentContradictionsHold(t *testing.T) {
+ for _, field := range []string{"key", "attempt", "binding", "created"} {
+ t.Run(field, func(t *testing.T) {
+ f := newSignupFixture(t, false)
+ f.seal(t, false)
+ f.candidate(t)
+ o := signupObservation{AccountID: 42, Sqid: f.a.Sqid, Key: f.a.Key, IntentID: f.i.Header.ID, Attempt: 1, BindingDigest: f.a.BindingDigest, CreatedAt: f.now, Body: f.a.Body}
+ switch field {
+ case "key":
+ o.Key = "wrong"
+ case "attempt":
+ o.Attempt = 2
+ case "binding":
+ o.BindingDigest = strings.Repeat("a", 64)
+ case "created":
+ o.CreatedAt = f.now.Add(time.Second)
+ }
+ r, e := f.s.observeSignupAttempt(f.i.Header.ID, f.scope, 1, o)
+ if e != nil || r.State != "held" {
+ t.Fatalf("same-intent contradiction classified foreign: %+v %v", r, e)
+ }
+ })
+ }
+}
+
+func TestSignupOwnObservationCannotBecomeForeign(t *testing.T) {
+ f := newSignupFixture(t, false)
+ f.seal(t, false)
+ f.candidate(t)
+ f.live(t)
+ foreign := signupObservation{AccountID: 42, Sqid: f.a.Sqid, Key: f.a.Key, CreatedAt: f.now.Add(time.Second), Body: []byte("replacement")}
+ for k := 0; k < 2; k++ {
+ r, e := f.s.observeSignupAttempt(f.i.Header.ID, f.scope, 1, foreign)
+ if e != nil || r.State != "held" {
+ t.Fatalf("own claim released on retry %d: %+v %v", k, r, e)
+ }
+ }
+ if _, e := f.complete(false); e == nil {
+ t.Fatal("held replacement birthed")
+ }
+ next := f.a
+ next.Number = 2
+ next.BindingDigest = signupAttemptBinding(f.i, next)
+ if _, e := f.s.recordSignupAttempt(f.raw, f.i.Header.ID, f.scope, next, f.now); !errors.Is(e, errOveragePreferenceConflict) {
+ t.Fatalf("own replacement advanced: %v", e)
+ }
+ f.live(t)
+ if _, e := f.complete(false); e != nil {
+ t.Fatalf("same own proof could not resolve hold: %v", e)
+ }
+}
+func TestSignupForeignCollisionDoesNotOwnOtherIntent(t *testing.T) {
+ f := newSignupFixture(t, false)
+ f.seal(t, false)
+ f.candidate(t)
+ other, e := f.s.createSignupDraft(f.raw, overageOperation(7), "other", f.scope, f.now)
+ if e != nil {
+ t.Fatal(e)
+ }
+ other, e = f.s.sealSignup(f.raw, other.Header.ID, f.scope, true, false, signupDigest("offer", nil), f.now)
+ if e != nil {
+ t.Fatal(e)
+ }
+ a := f.a
+ a.IntentID = other.Header.ID
+ a.BindingDigest = signupAttemptBinding(other, a)
+ if _, e = f.s.recordSignupAttempt(f.raw, a.IntentID, f.scope, a, f.now); e != nil {
+ t.Fatal(e)
+ }
+ if e = f.s.cancelSignup(f.raw, f.i.Header.ID, f.scope, f.now); e != nil {
+ t.Fatal(e)
+ }
+ o := signupObservation{AccountID: 42, Sqid: a.Sqid, Key: a.Key, CreatedAt: a.CreatedAt, Body: a.Body, IntentID: a.IntentID, Attempt: 1, BindingDigest: a.BindingDigest}
+ if r, e := f.s.observeSignupAttempt(f.i.Header.ID, f.scope, 1, o); e != nil || r.State != "collision" {
+ t.Fatalf("cancelled observer: %+v %v", r, e)
+ }
+ if _, e = f.s.observeSignupAttempt(a.IntentID, f.scope, 1, o); e != nil {
+ t.Fatal(e)
+ }
+ out, e := f.s.completeSignup(f.raw, a.IntentID, f.scope, other.Digest, false, f.now)
+ if e != nil || out.Historical.IntentID != a.IntentID {
+ t.Fatalf("foreign losing claim blocked winner: %+v %v", out, e)
+ }
+}
+func TestSignupRestartPreservesReceiptAndOFF(t *testing.T) {
+ f := newSignupFixture(t, true)
+ f.seal(t, true)
+ f.candidate(t)
+ f.live(t)
+ first, e := f.complete(true)
+ if e != nil {
+ t.Fatal(e)
+ }
+ if _, e = f.s.changeOveragePreference(overageChange(42, overageOwner, 1, false, 8), f.now.Add(time.Second)); e != nil {
+ t.Fatal(e)
+ }
+ var seq int
+ var name, path string
+ if e = f.s.db.QueryRow(`PRAGMA database_list`).Scan(&seq, &name, &path); e != nil {
+ t.Fatal(e)
+ }
+ if e = f.s.Close(); e != nil {
+ t.Fatal(e)
+ }
+ f.s = newOverageStore(t, path)
+ out, e := f.complete(true)
+ if e != nil || !bytes.Equal(signupBytes(first.Historical), signupBytes(out.Historical)) || out.Preference.RequestedOn || out.Preference.Revision != 2 {
+ t.Fatalf("restart changed history/current: %+v %v", out, e)
+ }
+}
+func TestSignupCancelBirthRace(t *testing.T) {
+ f := newSignupFixture(t, true)
+ f.seal(t, true)
+ f.candidate(t)
+ f.live(t)
+ start := make(chan struct{})
+ cancelled := make(chan error, 1)
+ birthed := make(chan error, 1)
+ go func() { <-start; cancelled <- f.s.cancelSignup(f.raw, f.i.Header.ID, f.scope, f.now) }()
+ go func() { <-start; _, e := f.complete(true); birthed <- e }()
+ close(start)
+ ce, be := <-cancelled, <-birthed
+ if ce == nil {
+ if !errors.Is(be, errOveragePreferenceConflict) || signupCount(t, f.s, `SELECT count(*) FROM accounts WHERE id=42`) != 0 {
+ t.Fatalf("cancel winner leaked birth: %v", be)
+ }
+ } else if be == nil {
+ if !errors.Is(ce, errOveragePreferenceConflict) || signupCount(t, f.s, `SELECT count(*) FROM account_birth_receipts`) != 1 {
+ t.Fatalf("birth winner cancelled: %v", ce)
+ }
+ } else {
+ t.Fatalf("neither serialized operation won: %v %v", ce, be)
+ }
+}
+func TestSignupAllUniqueKeysRefuseReplacement(t *testing.T) {
+ f := newSignupFixture(t, false)
+ f.seal(t, false)
+ f.candidate(t)
+ f.live(t)
+ if _, e := f.complete(false); e != nil {
+ t.Fatal(e)
+ }
+ for _, q := range []string{
+ `INSERT OR REPLACE INTO signup_intents(id,scope,subject,operation_id,header,offer,seal,digest,phase,attempt) SELECT 'different',scope,subject,operation_id,header,offer,seal,digest,phase,attempt FROM signup_intents`,
+ `INSERT OR REPLACE INTO account_birth_receipts SELECT 'different',scope,account_id,'different',receipt,digest FROM account_birth_receipts`,
+ `INSERT OR REPLACE INTO account_birth_receipts SELECT 'different',scope,999,sqid,receipt,digest FROM account_birth_receipts`,
+ } {
+ if _, e := f.s.db.Exec(q); e == nil {
+ t.Fatalf("alternate unique replacement allowed: %s", q)
+ }
+ }
+ if _, e := f.complete(false); e != nil {
+ t.Fatal(e)
+ }
+}
+
+func TestSignupAttemptBodyDigestDetectsCorruption(t *testing.T) {
+ f := newSignupFixture(t, false)
+ f.seal(t, false)
+ f.candidate(t)
+ var b []byte
+ if e := f.s.db.QueryRow(`SELECT tuple FROM signup_mint_attempts`).Scan(&b); e != nil {
+ t.Fatal(e)
+ }
+ var a signupAttempt
+ if e := signupDecode(b, &a); e != nil {
+ t.Fatal(e)
+ }
+ if a.BodyDigest == "" {
+ t.Fatal("missing independent body digest")
+ }
+ a.Body = []byte("different valid bytes")
+ if _, e := f.s.db.Exec(`DROP TRIGGER signup_attempts_update`); e != nil {
+ t.Fatal(e)
+ }
+ if _, e := f.s.db.Exec(`UPDATE signup_mint_attempts SET tuple=?`, signupBytes(a)); e != nil {
+ t.Fatal(e)
+ }
+ o := signupObservation{AccountID: 42, Sqid: a.Sqid, Key: a.Key, IntentID: a.IntentID, Attempt: a.Number, BindingDigest: a.BindingDigest, CreatedAt: a.CreatedAt, Body: a.Body}
+ if _, e := f.s.observeSignupAttempt(f.i.Header.ID, f.scope, 1, o); !errors.Is(e, errOveragePreferenceCorrupt) {
+ t.Fatalf("body corruption accepted: %v", e)
+ }
+ if signupCount(t, f.s, `SELECT count(*) FROM accounts WHERE id=42`) != 0 {
+ t.Fatal("corrupt tuple birthed account")
+ }
+}
+
+func TestSignupCollisionRevalidationPreservesSealedObservation(t *testing.T) {
+ f := newSignupFixture(t, false)
+ f.seal(t, false)
+ f.candidate(t)
+ o := signupObservation{AccountID: 42, Sqid: f.a.Sqid, Key: f.a.Key, CreatedAt: f.now.Add(-time.Second), Body: []byte("foreign original"), ETag: "first"}
+ first, e := f.s.observeSignupAttempt(f.i.Header.ID, f.scope, 1, o)
+ if e != nil || first.State != "collision" {
+ t.Fatalf("foreign: %+v %v", first, e)
+ }
+ o.Body = []byte("foreign renamed, different owner, tombstoned")
+ o.ETag = "second"
+ o.Retired = true
+ again, e := f.s.observeSignupAttempt(f.i.Header.ID, f.scope, 1, o)
+ if e != nil || !bytes.Equal(signupBytes(first), signupBytes(again)) {
+ t.Fatalf("immutable foreign binding replay: %+v %v", again, e)
+ }
+ o.CreatedAt = o.CreatedAt.Add(time.Second)
+ if _, e = f.s.observeSignupAttempt(f.i.Header.ID, f.scope, 1, o); !errors.Is(e, errOveragePreferenceConflict) {
+ t.Fatalf("foreign identity changed: %v", e)
+ }
+}
+
+func signupTableSnapshot(t *testing.T, s *Store, table string) []byte {
+ t.Helper()
+ rows, err := s.db.Query(`SELECT * FROM ` + table + ` ORDER BY 1,2`)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer rows.Close()
+ columns, err := rows.Columns()
+ if err != nil {
+ t.Fatal(err)
+ }
+ var records [][]any
+ for rows.Next() {
+ values := make([]any, len(columns))
+ dest := make([]any, len(columns))
+ for k := range values {
+ dest[k] = &values[k]
+ }
+ if err = rows.Scan(dest...); err != nil {
+ t.Fatal(err)
+ }
+ records = append(records, values)
+ }
+ if err = rows.Err(); err != nil {
+ t.Fatal(err)
+ }
+ return signupBytes(records)
+}
+func TestSignupRowidReplacementRefused(t *testing.T) {
+ for _, table := range []string{"signup_intents", "signup_mint_attempts", "account_birth_receipts"} {
+ for _, verb := range []string{"update", "insert"} {
+ t.Run(table+"/"+verb, func(t *testing.T) {
+ f := newSignupFixture(t, false)
+ f.seal(t, false)
+ f.candidate(t)
+ f.live(t)
+ if _, err := f.complete(false); err != nil {
+ t.Fatal(err)
+ }
+ other := f
+ var err error
+ other.i, err = f.s.createSignupDraft(f.raw, overageOperation(9), "other", f.scope, f.now)
+ if err != nil {
+ t.Fatal(err)
+ }
+ other.seal(t, false)
+ other.a = signupAttempt{IntentID: other.i.Header.ID, Number: 1, AccountID: 43, Sqid: domains.Sqid(43), CreatedAt: f.now, Body: []byte("other exact body")}
+ other.a.Key = domains.AccountManifestKey(other.a.Sqid)
+ other.a.BindingDigest = signupAttemptBinding(other.i, other.a)
+ if _, err = f.s.recordSignupAttempt(f.raw, other.i.Header.ID, f.scope, other.a, f.now); err != nil {
+ t.Fatal(err)
+ }
+ other.live(t)
+ if _, err = other.complete(false); err != nil {
+ t.Fatal(err)
+ }
+ before := signupTableSnapshot(t, f.s, table)
+ key := "intent_id"
+ if table == "signup_intents" {
+ key = "id"
+ }
+ q := `UPDATE OR REPLACE ` + table + ` SET rowid=(SELECT rowid FROM ` + table + ` WHERE ` + key + `=?) WHERE ` + key + `=?`
+ if verb == "insert" {
+ switch table {
+ case "signup_intents":
+ q = `INSERT OR REPLACE INTO signup_intents(rowid,id,scope,subject,operation_id,header,offer,seal,digest,phase,attempt) SELECT (SELECT rowid FROM signup_intents WHERE id=?),'replacement',scope,subject,'replacement',header,offer,seal,digest,phase,attempt FROM signup_intents WHERE id=?`
+ case "signup_mint_attempts":
+ q = `INSERT OR REPLACE INTO signup_mint_attempts(rowid,intent_id,number,account_id,tuple,state,observation) SELECT (SELECT rowid FROM signup_mint_attempts WHERE intent_id=?),'replacement',number,account_id,tuple,state,observation FROM signup_mint_attempts WHERE intent_id=?`
+ case "account_birth_receipts":
+ q = `INSERT OR REPLACE INTO account_birth_receipts(rowid,intent_id,scope,account_id,sqid,receipt,digest) SELECT (SELECT rowid FROM account_birth_receipts WHERE intent_id=?),'replacement',scope,999,'replacement',receipt,digest FROM account_birth_receipts WHERE intent_id=?`
+ }
+ }
+ if _, err = f.s.db.Exec(q, other.i.Header.ID, f.i.Header.ID); err == nil {
+ t.Errorf("explicit rowid %s accepted", verb)
+ }
+ if after := signupTableSnapshot(t, f.s, table); !bytes.Equal(before, after) {
+ t.Errorf("original %s records changed through rowid %s", table, verb)
+ }
+ if _, err = f.complete(false); err != nil {
+ t.Errorf("first original birth no longer replays: %v", err)
+ }
+ if _, err = other.complete(false); err != nil {
+ t.Errorf("second original birth no longer replays: %v", err)
+ }
+ })
+ }
+ }
+}
+
+func TestSignupHeldClaimSurvivesForeignRewrite(t *testing.T) {
+ for _, claim := range []string{"ambiguous-unmarked", "same-intent-contradiction"} {
+ for _, rewrite := range []string{"foreign-marker", "changed-created-at"} {
+ t.Run(claim+"/"+rewrite, func(t *testing.T) {
+ f := newSignupFixture(t, false)
+ f.seal(t, false)
+ f.candidate(t)
+ original := signupObservation{AccountID: 42, Sqid: f.a.Sqid, Key: f.a.Key, CreatedAt: f.now, Body: []byte("ambiguous own observation")}
+ if claim == "same-intent-contradiction" {
+ original.IntentID = f.i.Header.ID
+ original.Attempt = 1
+ original.BindingDigest = strings.Repeat("a", 64)
+ }
+ first, err := f.s.observeSignupAttempt(f.i.Header.ID, f.scope, 1, original)
+ if err != nil || first.State != "held" {
+ t.Fatalf("first held evidence: %+v %v", first, err)
+ }
+ changed := original
+ changed.IntentID = ""
+ changed.Attempt = 0
+ changed.BindingDigest = ""
+ changed.CreatedAt = f.now.Add(time.Second)
+ changed.Body = []byte("rewritten foreign identity")
+ if rewrite == "foreign-marker" {
+ changed.CreatedAt = f.now
+ changed.IntentID = strings.Repeat("b", 64)
+ changed.Attempt = 1
+ changed.BindingDigest = strings.Repeat("c", 64)
+ }
+ for k := 0; k < 2; k++ {
+ held, e := f.s.observeSignupAttempt(f.i.Header.ID, f.scope, 1, changed)
+ if e != nil || held.State != "held" || !bytes.Equal(signupBytes(held.Observation), signupBytes(first.Observation)) {
+ t.Fatalf("held provenance released on rewrite %d: %+v %v", k, held, e)
+ }
+ }
+ next := f.a
+ next.Number = 2
+ next.AccountID = 43
+ next.Sqid = domains.Sqid(43)
+ next.Key = domains.AccountManifestKey(next.Sqid)
+ next.BindingDigest = signupAttemptBinding(f.i, next)
+ if _, err = f.s.recordSignupAttempt(f.raw, f.i.Header.ID, f.scope, next, f.now); !errors.Is(err, errOveragePreferenceConflict) {
+ t.Fatalf("rewritten held claim advanced: %v", err)
+ }
+ if _, err = f.complete(false); !errors.Is(err, errOveragePreferenceConflict) {
+ t.Fatalf("held claim consumed: %v", err)
+ }
+ f.live(t)
+ if _, err = f.complete(false); err != nil {
+ t.Fatalf("exact own proof did not resolve held claim: %v", err)
+ }
+ })
+ }
+ }
+}
+func TestSignupHeldClaimResolvesRetired(t *testing.T) {
+ f := newSignupFixture(t, false)
+ f.seal(t, false)
+ f.candidate(t)
+ o := signupObservation{AccountID: 42, Sqid: f.a.Sqid, Key: f.a.Key, CreatedAt: f.now, Body: []byte("ambiguous")}
+ if r, err := f.s.observeSignupAttempt(f.i.Header.ID, f.scope, 1, o); err != nil || r.State != "held" {
+ t.Fatalf("hold: %+v %v", r, err)
+ }
+ o.IntentID = f.i.Header.ID
+ o.Attempt = 1
+ o.BindingDigest = f.a.BindingDigest
+ o.Retired = true
+ o.Body = []byte("own tombstone")
+ if r, err := f.s.observeSignupAttempt(f.i.Header.ID, f.scope, 1, o); err != nil || r.State != "observed_retired" {
+ t.Fatalf("retired proof: %+v %v", r, err)
+ }
+ if signupCount(t, f.s, `SELECT count(*) FROM signup_intents WHERE phase='retired'`) != 1 {
+ t.Fatal("held own retirement not terminal")
+ }
+}
+func signupCollisionChain(t *testing.T, count int64) signupFixture {
+ t.Helper()
+ f := newSignupFixture(t, true)
+ f.seal(t, true)
+ for n := int64(1); n <= count; n++ {
+ f.a = signupAttempt{IntentID: f.i.Header.ID, Number: n, AccountID: 41 + n, Sqid: domains.Sqid(41 + n), CreatedAt: f.now, Body: []byte(fmt.Sprintf("exact body %d", n))}
+ f.a.Key = domains.AccountManifestKey(f.a.Sqid)
+ f.a.BindingDigest = signupAttemptBinding(f.i, f.a)
+ if _, err := f.s.recordSignupAttempt(f.raw, f.i.Header.ID, f.scope, f.a, f.now); err != nil {
+ t.Fatal(err)
+ }
+ if n < count {
+ foreign := signupObservation{AccountID: f.a.AccountID, Sqid: f.a.Sqid, Key: f.a.Key, CreatedAt: f.now.Add(-time.Second), Body: []byte("foreign winner")}
+ if r, err := f.s.observeSignupAttempt(f.i.Header.ID, f.scope, n, foreign); err != nil || r.State != "collision" {
+ t.Fatalf("chain collision: %+v %v", r, err)
+ }
+ } else {
+ f.live(t)
+ }
+ }
+ return f
+}
+func TestSignupValidCollisionChainBirthAndOFFReplay(t *testing.T) {
+ f := signupCollisionChain(t, 5)
+ out, err := f.complete(true)
+ if err != nil || out.Historical.Attempt != 5 {
+ t.Fatalf("valid chain: %+v %v", out, err)
+ }
+ if _, err = f.s.changeOveragePreference(overageChange(f.a.AccountID, overageOwner, 1, false, 11), f.now.Add(time.Second)); err != nil {
+ t.Fatal(err)
+ }
+ retry, err := f.complete(true)
+ if err != nil || retry.Preference.RequestedOn || retry.Preference.Revision != 2 || !bytes.Equal(signupBytes(out.Historical), signupBytes(retry.Historical)) {
+ t.Fatalf("chain replay after OFF: %+v %v", retry, err)
+ }
+}
+func TestSignupBrokenPriorAttemptChainRefuses(t *testing.T) {
+ for _, stage := range []string{"completion", "replay-after-off"} {
+ for _, damage := range []string{"missing", "corrupt-tuple", "corrupt-observation", "not-collision"} {
+ t.Run(stage+"/"+damage, func(t *testing.T) {
+ f := signupCollisionChain(t, 3)
+ if stage == "replay-after-off" {
+ if _, err := f.complete(true); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := f.s.changeOveragePreference(overageChange(f.a.AccountID, overageOwner, 1, false, 12), f.now.Add(time.Second)); err != nil {
+ t.Fatal(err)
+ }
+ }
+ switch damage {
+ case "missing":
+ if _, err := f.s.db.Exec(`DROP TRIGGER signup_attempts_no_delete`); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := f.s.db.Exec(`DELETE FROM signup_mint_attempts WHERE number=1`); err != nil {
+ t.Fatal(err)
+ }
+ default:
+ if _, err := f.s.db.Exec(`DROP TRIGGER signup_attempts_update`); err != nil {
+ t.Fatal(err)
+ }
+ q := `UPDATE signup_mint_attempts SET tuple=X'00' WHERE number=1`
+ if damage == "corrupt-observation" {
+ q = `UPDATE signup_mint_attempts SET observation=X'00' WHERE number=1`
+ }
+ if damage == "not-collision" {
+ q = `UPDATE signup_mint_attempts SET state='held' WHERE number=1`
+ }
+ if _, err := f.s.db.Exec(q); err != nil {
+ t.Fatal(err)
+ }
+ }
+ before := map[string][]byte{}
+ for _, table := range []string{"signup_intents", "signup_mint_attempts", "accounts", "memberships", "overage_preferences", "overage_preference_receipts", "account_birth_receipts"} {
+ before[table] = signupTableSnapshot(t, f.s, table)
+ }
+ if _, err := f.complete(true); !errors.Is(err, errOveragePreferenceCorrupt) {
+ t.Errorf("broken prior chain accepted or misclassified: %v", err)
+ }
+ for table, want := range before {
+ if got := signupTableSnapshot(t, f.s, table); !bytes.Equal(want, got) {
+ t.Errorf("refused chain repaired/mutated %s", table)
+ }
+ }
+ if stage == "replay-after-off" {
+ p, err := f.s.getOveragePreference(f.a.AccountID, overageOwner)
+ if err != nil || p.RequestedOn || p.Revision != 2 {
+ t.Fatalf("OFF changed: %+v %v", p, err)
+ }
+ } else if signupCount(t, f.s, `SELECT count(*) FROM accounts WHERE id=?`, f.a.AccountID) != 0 {
+ t.Fatal("broken chain birthed account")
+ }
+ })
+ }
+ }
+}
+func TestSignupOutOfRangeAttemptChainRefuses(t *testing.T) {
+ f := signupCollisionChain(t, 5)
+ foreign := signupObservation{AccountID: f.a.AccountID, Sqid: f.a.Sqid, Key: f.a.Key, CreatedAt: f.now.Add(-time.Second), Body: []byte("foreign winner")}
+ if _, err := f.s.db.Exec(`UPDATE signup_mint_attempts SET state='collision',observation=? WHERE number=5`, signupBytes(foreign)); err != nil {
+ t.Fatal(err)
+ }
+ f.a.Number = 6
+ f.a.AccountID = 47
+ f.a.Sqid = domains.Sqid(47)
+ f.a.Key = domains.AccountManifestKey(f.a.Sqid)
+ f.a.BindingDigest = signupAttemptBinding(f.i, f.a)
+ f.a.BodyDigest = signupDigest("body", f.a.Body)
+ own := signupObservation{AccountID: f.a.AccountID, Sqid: f.a.Sqid, Key: f.a.Key, IntentID: f.i.Header.ID, Attempt: 6, BindingDigest: f.a.BindingDigest, CreatedAt: f.a.CreatedAt, Body: f.a.Body}
+ if _, err := f.s.db.Exec(`INSERT INTO signup_mint_attempts(intent_id,number,account_id,tuple,state,observation) VALUES(?,?,?,?,?,?)`, f.i.Header.ID, 6, f.a.AccountID, signupBytes(f.a), "observed_live", signupBytes(own)); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := f.s.db.Exec(`UPDATE signup_intents SET attempt=6 WHERE id=?`, f.i.Header.ID); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := f.complete(true); !errors.Is(err, errOveragePreferenceCorrupt) {
+ t.Fatalf("out-of-range chain accepted: %v", err)
+ }
+ if signupCount(t, f.s, `SELECT count(*) FROM accounts WHERE id=?`, f.a.AccountID) != 0 || signupCount(t, f.s, `SELECT count(*) FROM overage_preferences`) != 0 {
+ t.Fatal("out-of-range chain consumed")
+ }
+}
diff --git a/internal/console/store.go b/internal/console/store.go
index 564ad7591..57118b659 100644
--- a/internal/console/store.go
+++ b/internal/console/store.go
@@ -135,7 +135,8 @@ func setConsoleSQLitePragmas(q url.Values) {
// Close releases the underlying database handle.
func (s *Store) Close() error { return s.db.Close() }
-// migrate is additive-only (CREATE TABLE IF NOT EXISTS); future columns go
+// migrate is additive-only except for the apps primary-key and immutable
+// overage-receipt WITHOUT ROWID rebuilds; future columns go
// in the ALTER list, whose "duplicate column" error is swallowed so the list
// stays idempotent.
func (s *Store) migrate() error {
@@ -437,7 +438,10 @@ func (s *Store) migrate() error {
if err := s.migrateBilling(); err != nil {
return err
}
- return s.migrateOveragePreference()
+ if err := s.migrateOveragePreference(); err != nil {
+ return err
+ }
+ return s.migrateSignupRecords()
}
// migrateAppsKey moves the apps table from PRIMARY KEY(app) — a GLOBAL name
@@ -816,6 +820,13 @@ func (s *Store) CreateAccount(id int64, sqid, name, ownerAddress string, now tim
return err
}
defer tx.Rollback()
+ if err := createAccountTx(tx, id, sqid, name, ownerAddress, now); err != nil {
+ return err
+ }
+ return tx.Commit()
+}
+
+func createAccountTx(tx *sql.Tx, id int64, sqid, name, ownerAddress string, now time.Time) error {
if _, err := lockMembershipWritesTx(tx); err != nil {
return err
}
@@ -832,7 +843,7 @@ func (s *Store) CreateAccount(id int64, sqid, name, ownerAddress string, now tim
id, ownerAddress, RoleOwner, now.Unix()); err != nil {
return err
}
- return tx.Commit()
+ return nil
}
// EnsureAccount records an account the console did NOT mint — one that already