mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
dexprotocol: Reserve binds to the accepted parent — rule 1 enforced
Settle and Release already required a verified AcceptedBlock; Reserve did not, so a reservation could still be created without any proof that its C parent was accepted. That left the abandoned-parent case reachable through the one door that had not been closed. Reserve(parent AcceptedBlock, v VerifiedExecution) now enforces both halves of rule 1: the parent must carry the verified sentinel, and the execution must be scoped to exactly that parent. Both arguments are unforgeable outside this package, so a reservation can be taken neither against an unauthenticated execution nor against an unproven C parent. 25 tests passing. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -67,6 +67,18 @@ func acceptedConsuming(t *testing.T, id, parent ids.ID, consumed ...ids.ID) (Acc
|
||||
return ab, set.Prove(target)
|
||||
}
|
||||
|
||||
// acceptedParent returns a verified AcceptedBlock for P — the parent every sample
|
||||
// execution is scoped to. Rule 1 requires one to reserve at all.
|
||||
func acceptedParent(t *testing.T) AcceptedBlock {
|
||||
t.Helper()
|
||||
msg := EncodeAcceptedBlock(testParent, ids.ID{0x0B}, 1, NewExecSet().Root())
|
||||
ab, err := VerifyAcceptedBlock(msg, []byte("cert"), okVerifier{})
|
||||
if err != nil {
|
||||
t.Fatalf("VerifyAcceptedBlock(parent): %v", err)
|
||||
}
|
||||
return ab
|
||||
}
|
||||
|
||||
func verified(t *testing.T, e Execution) VerifiedExecution {
|
||||
t.Helper()
|
||||
v, err := VerifyExecution(witnessFor(e), VerifyContext{
|
||||
@@ -209,7 +221,7 @@ func TestSettleAndReleaseAreMutuallyExclusiveAndTerminal(t *testing.T) {
|
||||
t.Run("settled is terminal", func(t *testing.T) {
|
||||
l := NewLedger()
|
||||
e := sampleExecution()
|
||||
if _, err := l.Reserve(verified(t, e)); err != nil {
|
||||
if _, err := l.Reserve(acceptedParent(t), verified(t, e)); err != nil {
|
||||
t.Fatalf("reserve: %v", err)
|
||||
}
|
||||
if _, err := l.Settle(e.ExecID, accepted, incl); err != nil {
|
||||
@@ -232,7 +244,7 @@ func TestSettleAndReleaseAreMutuallyExclusiveAndTerminal(t *testing.T) {
|
||||
t.Run("released is terminal", func(t *testing.T) {
|
||||
l := NewLedger()
|
||||
e := sampleExecution()
|
||||
if _, err := l.Reserve(verified(t, e)); err != nil {
|
||||
if _, err := l.Reserve(acceptedParent(t), verified(t, e)); err != nil {
|
||||
t.Fatalf("reserve: %v", err)
|
||||
}
|
||||
if _, err := l.Release(e.ExecID, qEmpty, nonIncl); err != nil {
|
||||
@@ -254,16 +266,16 @@ func TestReplayedCertificateCannotReReserve(t *testing.T) {
|
||||
e := sampleExecution()
|
||||
accepted, incl := acceptedConsuming(t, testCBlock, testParent, e.ExecID)
|
||||
l := NewLedger()
|
||||
if _, err := l.Reserve(verified(t, e)); err != nil {
|
||||
if _, err := l.Reserve(acceptedParent(t), verified(t, e)); err != nil {
|
||||
t.Fatalf("reserve: %v", err)
|
||||
}
|
||||
if _, err := l.Settle(e.ExecID, accepted, incl); err != nil {
|
||||
t.Fatalf("settle: %v", err)
|
||||
}
|
||||
if _, err := l.Reserve(verified(t, e)); !errors.Is(err, ErrTerminalExists) {
|
||||
if _, err := l.Reserve(acceptedParent(t), verified(t, e)); !errors.Is(err, ErrTerminalExists) {
|
||||
t.Fatalf("re-reserving a terminal ExecID must be refused, got %v", err)
|
||||
}
|
||||
if _, err := l.Reserve(verified(t, e)); !errors.Is(err, ErrTerminalExists) {
|
||||
if _, err := l.Reserve(acceptedParent(t), verified(t, e)); !errors.Is(err, ErrTerminalExists) {
|
||||
t.Fatalf("still refused on a second attempt, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -271,10 +283,10 @@ func TestReplayedCertificateCannotReReserve(t *testing.T) {
|
||||
func TestDoubleReserveRefused(t *testing.T) {
|
||||
l := NewLedger()
|
||||
e := sampleExecution()
|
||||
if _, err := l.Reserve(verified(t, e)); err != nil {
|
||||
if _, err := l.Reserve(acceptedParent(t), verified(t, e)); err != nil {
|
||||
t.Fatalf("reserve: %v", err)
|
||||
}
|
||||
if _, err := l.Reserve(verified(t, e)); !errors.Is(err, ErrAlreadyOpen) {
|
||||
if _, err := l.Reserve(acceptedParent(t), verified(t, e)); !errors.Is(err, ErrAlreadyOpen) {
|
||||
t.Fatalf("double reserve must be refused, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -289,7 +301,7 @@ func TestSettleChecksTheAcceptedBlocksParent(t *testing.T) {
|
||||
accepted, incl := acceptedConsuming(t, testCBlock, wrongParent, e.ExecID)
|
||||
|
||||
l := NewLedger()
|
||||
if _, err := l.Reserve(verified(t, e)); err != nil {
|
||||
if _, err := l.Reserve(acceptedParent(t), verified(t, e)); err != nil {
|
||||
t.Fatalf("reserve: %v", err)
|
||||
}
|
||||
if _, err := l.Settle(e.ExecID, accepted, incl); !errors.Is(err, ErrWrongSuccessor) {
|
||||
@@ -308,13 +320,13 @@ func TestStorageDomainsAreDistinct(t *testing.T) {
|
||||
qEmpty, nonIncl := acceptedConsuming(t, testCBlock, testParent)
|
||||
|
||||
l := NewLedger()
|
||||
r, _ := l.Reserve(verified(t, e))
|
||||
r, _ := l.Reserve(acceptedParent(t), verified(t, e))
|
||||
tr, err := l.Settle(e.ExecID, accepted, incl)
|
||||
if err != nil {
|
||||
t.Fatalf("settle: %v", err)
|
||||
}
|
||||
l2 := NewLedger()
|
||||
r2, _ := l2.Reserve(verified(t, e))
|
||||
r2, _ := l2.Reserve(acceptedParent(t), verified(t, e))
|
||||
rel, err := l2.Release(e.ExecID, qEmpty, nonIncl)
|
||||
if err != nil {
|
||||
t.Fatalf("release: %v", err)
|
||||
|
||||
@@ -125,7 +125,7 @@ func TestSettleRequiresInclusionReleaseRequiresNonInclusion(t *testing.T) {
|
||||
_, nonIncl := acceptedConsuming(t, testCBlock, testParent)
|
||||
|
||||
l := NewLedger()
|
||||
if _, err := l.Reserve(verified(t, e)); err != nil {
|
||||
if _, err := l.Reserve(acceptedParent(t), verified(t, e)); err != nil {
|
||||
t.Fatalf("reserve: %v", err)
|
||||
}
|
||||
if _, err := l.Settle(e.ExecID, consumed, nonIncl); !errors.Is(err, ErrWrongProof) {
|
||||
@@ -148,7 +148,7 @@ func TestUnverifiedParentCannotSettleOrRelease(t *testing.T) {
|
||||
_, incl := acceptedConsuming(t, testCBlock, testParent, e.ExecID)
|
||||
|
||||
l := NewLedger()
|
||||
if _, err := l.Reserve(verified(t, e)); err != nil {
|
||||
if _, err := l.Reserve(acceptedParent(t), verified(t, e)); err != nil {
|
||||
t.Fatalf("reserve: %v", err)
|
||||
}
|
||||
var forged AcceptedBlock // the zero value an outside package can build
|
||||
@@ -199,7 +199,7 @@ func TestUselessButResolvableReservation(t *testing.T) {
|
||||
q, nonIncl := acceptedConsuming(t, testCBlock, testParent)
|
||||
|
||||
l := NewLedger()
|
||||
if _, err := l.Reserve(verified(t, e)); err != nil {
|
||||
if _, err := l.Reserve(acceptedParent(t), verified(t, e)); err != nil {
|
||||
t.Fatalf("reserve: %v", err)
|
||||
}
|
||||
if _, err := l.Release(e.ExecID, q, nonIncl); err != nil {
|
||||
@@ -246,3 +246,47 @@ func TestLeafAndNodeDomainsAreSeparated(t *testing.T) {
|
||||
t.Fatal("the empty leaf must be distinct from any populated leaf")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRule1_ReserveRequiresTheAcceptedParent pins rule 1 of the proof system:
|
||||
//
|
||||
// Accepted(P) permits Reserve(E) with E.CParent == P
|
||||
//
|
||||
// Both halves are enforced. Without the first, a reservation could be taken
|
||||
// against a merely proposed block, which is the abandoned-parent case the
|
||||
// accepted-only rule exists to make nonexistent. Without the second, a reservation
|
||||
// could be taken against the wrong opportunity entirely.
|
||||
func TestRule1_ReserveRequiresTheAcceptedParent(t *testing.T) {
|
||||
e := sampleExecution() // scoped to testParent
|
||||
|
||||
t.Run("unverified parent is refused", func(t *testing.T) {
|
||||
l := NewLedger()
|
||||
var proposed AcceptedBlock // the zero value an outside package can build
|
||||
if _, err := l.Reserve(proposed, verified(t, e)); !errors.Is(err, ErrAcceptedUnverified) {
|
||||
t.Fatalf("reserving against an unverified parent must be refused, got %v", err)
|
||||
}
|
||||
if r, _, _ := l.Counts(); r != 0 {
|
||||
t.Fatal("a refused reserve must not enter the ledger")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrong accepted parent is refused", func(t *testing.T) {
|
||||
other := testParent
|
||||
other[0] ^= 1
|
||||
msg := EncodeAcceptedBlock(other, ids.ID{0x0B}, 1, NewExecSet().Root())
|
||||
wrong, err := VerifyAcceptedBlock(msg, []byte("cert"), okVerifier{})
|
||||
if err != nil {
|
||||
t.Fatalf("VerifyAcceptedBlock: %v", err)
|
||||
}
|
||||
l := NewLedger()
|
||||
if _, err := l.Reserve(wrong, verified(t, e)); !errors.Is(err, ErrWrongParent) {
|
||||
t.Fatalf("reserving against the wrong accepted parent must be refused, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("the accepted parent it is scoped to succeeds", func(t *testing.T) {
|
||||
l := NewLedger()
|
||||
if _, err := l.Reserve(acceptedParent(t), verified(t, e)); err != nil {
|
||||
t.Fatalf("reserving against the correct accepted parent must succeed: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -93,12 +93,26 @@ func (r Released) ExecID() ids.ID { return r.exec.ExecID }
|
||||
func (r Released) CBlock() ids.ID { return r.cBlock }
|
||||
func (r Released) Key() string { return domainReleased + r.exec.ExecID.String() }
|
||||
|
||||
// Reserve admits a verified execution into the reserved domain. It is the ONLY
|
||||
// entry point into the lifecycle and requires a VerifiedExecution — which cannot be
|
||||
// constructed outside this package — so a reservation can never be taken against an
|
||||
// execution nobody authenticated.
|
||||
func Reserve(v VerifiedExecution) ReservedExecution {
|
||||
return ReservedExecution{exec: v.Execution()}
|
||||
// Reserve admits a verified execution into the reserved domain, AGAINST AN ACCEPTED
|
||||
// C PARENT. This is rule 1 of the proof system:
|
||||
//
|
||||
// Accepted(P) permits Reserve(E) with E.CParent == P
|
||||
//
|
||||
// Both arguments are unforgeable: VerifiedExecution cannot be constructed outside
|
||||
// this package, and AcceptedBlock only comes from VerifyAcceptedBlock. So a
|
||||
// reservation can be taken neither against an execution nobody authenticated nor
|
||||
// against a C parent nobody proved accepted — which is what makes the
|
||||
// abandoned-parent case not merely handled but nonexistent.
|
||||
func Reserve(parent AcceptedBlock, v VerifiedExecution) (ReservedExecution, error) {
|
||||
if !parent.verified {
|
||||
return ReservedExecution{}, ErrAcceptedUnverified
|
||||
}
|
||||
e := v.Execution()
|
||||
if e.CParent != parent.ID() {
|
||||
return ReservedExecution{}, fmt.Errorf("%w: execution scoped to %s, reserving against accepted %s",
|
||||
ErrWrongParent, e.CParent, parent.ID())
|
||||
}
|
||||
return ReservedExecution{exec: e}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -107,6 +121,7 @@ var (
|
||||
ErrAlreadyOpen = errors.New("dexprotocol: an execution is already reserved under that id")
|
||||
ErrTerminalExists = errors.New("dexprotocol: a terminal execution already exists under that id")
|
||||
ErrWrongProof = errors.New("dexprotocol: the proof does not match the transition being attempted")
|
||||
ErrWrongParent = errors.New("dexprotocol: the execution is not scoped to this accepted C parent")
|
||||
)
|
||||
|
||||
// THE ENTIRE PROOF SYSTEM, THREE RULES:
|
||||
@@ -214,7 +229,7 @@ func (l *Ledger) terminalExists(id ids.ID) bool {
|
||||
// Reserve admits a verified execution. Refuses an id that is already reserved or
|
||||
// already terminal — the latter is what stops a replayed certificate from
|
||||
// re-reserving liquidity that was already settled or released.
|
||||
func (l *Ledger) Reserve(v VerifiedExecution) (ReservedExecution, error) {
|
||||
func (l *Ledger) Reserve(parent AcceptedBlock, v VerifiedExecution) (ReservedExecution, error) {
|
||||
id := v.Execution().ExecID
|
||||
if l.terminalExists(id) {
|
||||
return ReservedExecution{}, fmt.Errorf("%w: %s", ErrTerminalExists, id)
|
||||
@@ -222,7 +237,10 @@ func (l *Ledger) Reserve(v VerifiedExecution) (ReservedExecution, error) {
|
||||
if _, ok := l.reserved[id]; ok {
|
||||
return ReservedExecution{}, fmt.Errorf("%w: %s", ErrAlreadyOpen, id)
|
||||
}
|
||||
r := Reserve(v)
|
||||
r, err := Reserve(parent, v)
|
||||
if err != nil {
|
||||
return ReservedExecution{}, err
|
||||
}
|
||||
l.reserved[id] = r
|
||||
return r, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user