Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions internal/repository/postgres/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,16 @@ func (r *PostgresRepository) ListOutputs(
}

if f.VoucherAddress != nil {
// A destination-address filter is only meaningful for output methods
// that carry a destination (Voucher, DelegateCallVoucher); bytes 17..36
// of other outputs are arbitrary payload. The selector IN list, with
// inline literals, is also what lets the planner prove the partial
// predicate of output_raw_data_address_idx.
conditions = append(conditions,
SubstrBytea(table.Output.RawData, 1, 4).IN(
ByteaLiteral(voucherSelector),
ByteaLiteral(delegateCallVoucherSelector),
),
SubstrBytea(table.Output.RawData, 17, 20).EQ(postgres.Bytea(f.VoucherAddress.Bytes())),
)
}
Expand Down
19 changes: 16 additions & 3 deletions internal/repository/postgres/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,22 @@ func countFromTx(ctx context.Context, tx pgx.Tx, countStmt postgres.SelectStatem
return total, err
}

// SubstrBytea returns a SUBSTR expression properly typed as ByteaExpression.
// SubstrBytea returns a substring expression properly typed as ByteaExpression.
// It must render exactly as `substring(col FROM n FOR m)` with inline literals:
// PostgreSQL matches expression indexes structurally (by function OID and
// argument tree), so the schema's `substring(... FROM ... FOR ...)` indexes are
// only usable when the query emits the same function with constant arguments.
// `SUBSTR(col, $1, $2)` is a different catalog function and bypasses them.
func SubstrBytea(col postgres.ColumnBytea, from, count int64) postgres.ByteaExpression {
qualified := pgx.Identifier{col.TableName(), col.Name()}.Sanitize()
raw := fmt.Sprintf("SUBSTR(%s, #from, #count)", qualified)
return postgres.RawBytea(raw, postgres.RawArgs{"#from": from, "#count": count})
raw := fmt.Sprintf("substring(%s FROM %d FOR %d)", qualified, from, count)
return postgres.RawBytea(raw)
}

// ByteaLiteral renders b as an inline bytea literal instead of a bind
// parameter. Inline literals are required where the planner must prove a
// partial-index predicate at plan time (e.g. output_raw_data_address_idx):
// a generic plan cannot prove implication from a parameterized IN list.
func ByteaLiteral(b []byte) postgres.ByteaExpression {
return postgres.RawBytea(fmt.Sprintf(`'\x%x'::bytea`, b))
}
29 changes: 24 additions & 5 deletions internal/repository/repotest/output_test_cases.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,27 +244,46 @@ func (s *OutputSuite) TestListOutputs() {
s.Run("FilterByVoucherAddress", func() {
seed := Seed(s.Ctx, s.T(), s.Repo)

// VoucherAddress filter uses SUBSTR(raw_data, 17, 20)
// to extract a 20-byte address at bytes 17-36 (1-indexed)
// VoucherAddress filter matches substring(raw_data FROM 17 FOR 20)
// (the ABI head destination) but only on voucher-typed outputs:
// bytes 17-36 of other output types are arbitrary payload.
voucherSelector := []byte{0x23, 0x7a, 0x81, 0x6f}
delegateCallVoucherSelector := []byte{0x10, 0x32, 0x1e, 0x8b}
noticeSelector := []byte{0xc2, 0x58, 0xd6, 0xe5}

Comment thread
vfusco marked this conversation as resolved.
voucherAddr := UniqueAddress()
rawWithVoucher := make([]byte, 64)
copy(rawWithVoucher[0:4], voucherSelector)
copy(rawWithVoucher[16:36], voucherAddr.Bytes())

otherAddr := UniqueAddress()
rawWithOther := make([]byte, 64)
copy(rawWithOther[0:4], voucherSelector)
copy(rawWithOther[16:36], otherAddr.Bytes())

// A notice whose payload happens to contain the searched address at
// bytes 17-36 must not match the voucher-address filter.
rawNoticeLookalike := make([]byte, 64)
copy(rawNoticeLookalike[0:4], noticeSelector)
copy(rawNoticeLookalike[16:36], voucherAddr.Bytes())

// Delegate-call vouchers carry a destination too and must match.
rawWithDelegateCall := make([]byte, 64)
copy(rawWithDelegateCall[0:4], delegateCallVoucherSelector)
copy(rawWithDelegateCall[16:36], voucherAddr.Bytes())

s.storeAdvanceResult(seed.App.ID, 0, 0,
[][]byte{rawWithVoucher, rawWithOther}, nil)
[][]byte{rawWithVoucher, rawWithOther, rawNoticeLookalike, rawWithDelegateCall}, nil)

outputs, total, err := s.Repo.ListOutputs(
s.Ctx, seed.App.IApplicationAddress.String(),
Comment thread
vfusco marked this conversation as resolved.
repository.OutputFilter{VoucherAddress: &voucherAddr},
repository.Pagination{Limit: 10}, false)
s.Require().NoError(err)
s.Len(outputs, 1)
s.Equal(uint64(1), total)
s.Len(outputs, 2)
s.Equal(uint64(2), total)
s.Equal(rawWithVoucher, outputs[0].RawData)
s.Equal(rawWithDelegateCall, outputs[1].RawData)
})

s.Run("Pagination", func() {
Expand Down
Loading