Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

services/horizon: Support new CAP-21 transaction conditions #4297

Merged
merged 22 commits into from
Mar 25, 2022
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
79 changes: 51 additions & 28 deletions protocols/horizon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -501,34 +501,57 @@ type Transaction struct {
// When TransactionSuccess is removed from the SDKs we can remove this HAL link
Transaction hal.Link `json:"transaction"`
} `json:"_links"`
ID string `json:"id"`
PT string `json:"paging_token"`
Successful bool `json:"successful"`
Hash string `json:"hash"`
Ledger int32 `json:"ledger"`
LedgerCloseTime time.Time `json:"created_at"`
Account string `json:"source_account"`
AccountMuxed string `json:"account_muxed,omitempty"`
AccountMuxedID uint64 `json:"account_muxed_id,omitempty,string"`
AccountSequence string `json:"source_account_sequence"`
FeeAccount string `json:"fee_account"`
FeeAccountMuxed string `json:"fee_account_muxed,omitempty"`
FeeAccountMuxedID uint64 `json:"fee_account_muxed_id,omitempty,string"`
FeeCharged int64 `json:"fee_charged,string"`
MaxFee int64 `json:"max_fee,string"`
OperationCount int32 `json:"operation_count"`
EnvelopeXdr string `json:"envelope_xdr"`
ResultXdr string `json:"result_xdr"`
ResultMetaXdr string `json:"result_meta_xdr"`
FeeMetaXdr string `json:"fee_meta_xdr"`
MemoType string `json:"memo_type"`
MemoBytes string `json:"memo_bytes,omitempty"`
Memo string `json:"memo,omitempty"`
Signatures []string `json:"signatures"`
ValidAfter string `json:"valid_after,omitempty"`
ValidBefore string `json:"valid_before,omitempty"`
FeeBumpTransaction *FeeBumpTransaction `json:"fee_bump_transaction,omitempty"`
InnerTransaction *InnerTransaction `json:"inner_transaction,omitempty"`
ID string `json:"id"`
PT string `json:"paging_token"`
Successful bool `json:"successful"`
Hash string `json:"hash"`
Ledger int32 `json:"ledger"`
LedgerCloseTime time.Time `json:"created_at"`
Account string `json:"source_account"`
AccountMuxed string `json:"account_muxed,omitempty"`
AccountMuxedID uint64 `json:"account_muxed_id,omitempty,string"`
AccountSequence string `json:"source_account_sequence"`
FeeAccount string `json:"fee_account"`
FeeAccountMuxed string `json:"fee_account_muxed,omitempty"`
FeeAccountMuxedID uint64 `json:"fee_account_muxed_id,omitempty,string"`
FeeCharged int64 `json:"fee_charged,string"`
MaxFee int64 `json:"max_fee,string"`
OperationCount int32 `json:"operation_count"`
EnvelopeXdr string `json:"envelope_xdr"`
ResultXdr string `json:"result_xdr"`
ResultMetaXdr string `json:"result_meta_xdr"`
FeeMetaXdr string `json:"fee_meta_xdr"`
MemoType string `json:"memo_type"`
MemoBytes string `json:"memo_bytes,omitempty"`
Memo string `json:"memo,omitempty"`
Signatures []string `json:"signatures"`
// Action needed in release: horizon-v3.0.0: remove valid_(after|before)
ValidAfter string `json:"valid_after,omitempty"`
ValidBefore string `json:"valid_before,omitempty"`
Preconditions *TransactionPreconditions `json:"preconditions,omitempty"`
FeeBumpTransaction *FeeBumpTransaction `json:"fee_bump_transaction,omitempty"`
InnerTransaction *InnerTransaction `json:"inner_transaction,omitempty"`
}

type TransactionPreconditions struct {
Timebounds *TransactionPreconditionsTimebounds `json:"timebounds,omitempty"`
Ledgerbounds *TransactionPreconditionsLedgerbounds `json:"ledgerbounds,omitempty"`

MinAccountSequence string `json:"min_account_sequence,omitempty"`
MinAccountSequenceAge string `json:"min_account_sequence_age,omitempty"`
MinAccountSequenceLedgerGap uint32 `json:"min_account_sequence_ledger_gap,omitempty"`

ExtraSigners []string `json:"extra_signers,omitempty"`
}

type TransactionPreconditionsTimebounds struct {
MinTime string `json:"min_time,omitempty"`
MaxTime string `json:"max_time,omitempty"`
}

type TransactionPreconditionsLedgerbounds struct {
MinLedger uint32 `json:"min_ledger"`
MaxLedger uint32 `json:"max_ledger"`
}

// FeeBumpTransaction contains information about a fee bump transaction
Expand Down
3 changes: 3 additions & 0 deletions services/horizon/internal/db2/history/fee_bump_scenario.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

sq "github.com/Masterminds/squirrel"
"github.com/guregu/null"
"github.com/lib/pq"

"github.com/stellar/go/ingest"
"github.com/stellar/go/network"
Expand Down Expand Up @@ -312,6 +313,8 @@ func FeeBumpScenario(tt *test.T, q *Q, successful bool) FeeBumpFixture {
MemoType: "none",
Memo: null.NewString("", false),
TimeBounds: TimeBounds{Lower: null.IntFrom(2), Upper: null.IntFrom(4)},
LedgerBounds: LedgerBounds{Null: true},
ExtraSigners: pq.StringArray{},
Signatures: signatures(fixture.Envelope.FeeBumpSignatures()),
InnerSignatures: signatures(fixture.Envelope.Signatures()),
Successful: successful,
Expand Down
9 changes: 9 additions & 0 deletions services/horizon/internal/db2/history/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,15 @@ type Transaction struct {
TransactionWithoutLedger
}

func (t *Transaction) HasPreconditions() bool {
return !t.TimeBounds.Null ||
!t.LedgerBounds.Null ||
t.MinAccountSequence.Valid ||
t.MinAccountSequenceAge.Valid ||
t.MinAccountSequenceLedgerGap.Valid ||
len(t.ExtraSigners) > 0
}

// TransactionsQ is a helper struct to aid in configuring queries that loads
// slices of transaction structs.
type TransactionsQ struct {
Expand Down
7 changes: 6 additions & 1 deletion services/horizon/internal/db2/history/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,12 @@ var selectTransaction = sq.Select(
"ht.signatures, " +
"ht.memo_type, " +
"ht.memo, " +
"time_bounds, " +
"ht.time_bounds, " +
"ht.ledger_bounds, " +
"ht.min_account_sequence, " +
"ht.min_account_sequence_age, " +
"ht.min_account_sequence_ledger_gap, " +
"ht.extra_signers, " +
"hl.closed_at AS ledger_close_time, " +
"ht.inner_transaction_hash, " +
"ht.fee_account, " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ package history

import (
"context"
"database/sql/driver"
"encoding/base64"
"encoding/hex"
"fmt"
"math"
"strconv"
"strings"
"time"
Expand All @@ -17,7 +15,6 @@ import (
"github.com/stellar/go/ingest"
"github.com/stellar/go/services/horizon/internal/utf8"
"github.com/stellar/go/support/db"
"github.com/stellar/go/support/errors"
"github.com/stellar/go/toid"
"github.com/stellar/go/xdr"
)
Expand Down Expand Up @@ -60,91 +57,6 @@ func (i *transactionBatchInsertBuilder) Exec(ctx context.Context) error {
return i.builder.Exec(ctx)
}

// TimeBounds represents the time bounds of a Stellar transaction
type TimeBounds struct {
Null bool
Upper null.Int
Lower null.Int
}

// Scan implements the database/sql Scanner interface.
func (t *TimeBounds) Scan(src interface{}) error {
if src == nil {
*t = TimeBounds{Null: true}
return nil
}

var rangeText string
switch src := src.(type) {
case string:
rangeText = src
case []byte:
rangeText = string(src)
default:
return errors.Errorf("cannot scan %T", src)
}

rangeText = strings.TrimSpace(rangeText)
if len(rangeText) < 3 {
return errors.Errorf("range is invalid %s", rangeText)
}
inner := rangeText[1 : len(rangeText)-1]
parts := strings.Split(inner, ",")
if len(parts) != 2 {
return errors.Errorf("%s does not have 2 comma separated values", rangeText)
}

lower, upper := parts[0], parts[1]
if len(lower) > 0 {
if err := t.Lower.Scan(lower); err != nil {
return errors.Wrap(err, "cannot parse lower bound")
}
}
if len(upper) > 0 {
if err := t.Upper.Scan(upper); err != nil {
return errors.Wrap(err, "cannot parse upper bound")
}
}

return nil
}

// Value implements the database/sql/driver Valuer interface.
func (t TimeBounds) Value() (driver.Value, error) {
if t.Null {
return nil, nil
}

if !t.Upper.Valid {
return fmt.Sprintf("[%d,)", t.Lower.Int64), nil
}

return fmt.Sprintf("[%d, %d)", t.Lower.Int64, t.Upper.Int64), nil
}

func formatTimeBounds(transaction ingest.LedgerTransaction) TimeBounds {
timeBounds := transaction.Envelope.TimeBounds()
if timeBounds == nil {
return TimeBounds{Null: true}
}

if timeBounds.MaxTime == 0 {
return TimeBounds{
Lower: null.IntFrom(int64(timeBounds.MinTime)),
}
}

maxTime := timeBounds.MaxTime
if maxTime > math.MaxInt64 {
maxTime = math.MaxInt64
}

return TimeBounds{
Lower: null.IntFrom(int64(timeBounds.MinTime)),
Upper: null.IntFrom(int64(maxTime)),
}
}

func signatures(xdrSignatures []xdr.DecoratedSignature) pq.StringArray {
signatures := make([]string, len(xdrSignatures))
for i, sig := range xdrSignatures {
Expand Down Expand Up @@ -204,31 +116,36 @@ func memo(transaction ingest.LedgerTransaction) null.String {

type TransactionWithoutLedger struct {
TotalOrderID
TransactionHash string `db:"transaction_hash"`
LedgerSequence int32 `db:"ledger_sequence"`
ApplicationOrder int32 `db:"application_order"`
Account string `db:"account"`
AccountMuxed null.String `db:"account_muxed"`
AccountSequence string `db:"account_sequence"`
MaxFee int64 `db:"max_fee"`
FeeCharged int64 `db:"fee_charged"`
OperationCount int32 `db:"operation_count"`
TxEnvelope string `db:"tx_envelope"`
TxResult string `db:"tx_result"`
TxMeta string `db:"tx_meta"`
TxFeeMeta string `db:"tx_fee_meta"`
Signatures pq.StringArray `db:"signatures"`
MemoType string `db:"memo_type"`
Memo null.String `db:"memo"`
TimeBounds TimeBounds `db:"time_bounds"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
Successful bool `db:"successful"`
FeeAccount null.String `db:"fee_account"`
FeeAccountMuxed null.String `db:"fee_account_muxed"`
InnerTransactionHash null.String `db:"inner_transaction_hash"`
NewMaxFee null.Int `db:"new_max_fee"`
InnerSignatures pq.StringArray `db:"inner_signatures"`
TransactionHash string `db:"transaction_hash"`
LedgerSequence int32 `db:"ledger_sequence"`
ApplicationOrder int32 `db:"application_order"`
Account string `db:"account"`
AccountMuxed null.String `db:"account_muxed"`
AccountSequence string `db:"account_sequence"`
MaxFee int64 `db:"max_fee"`
FeeCharged int64 `db:"fee_charged"`
OperationCount int32 `db:"operation_count"`
TxEnvelope string `db:"tx_envelope"`
TxResult string `db:"tx_result"`
TxMeta string `db:"tx_meta"`
TxFeeMeta string `db:"tx_fee_meta"`
Signatures pq.StringArray `db:"signatures"`
MemoType string `db:"memo_type"`
Memo null.String `db:"memo"`
TimeBounds TimeBounds `db:"time_bounds"`
LedgerBounds LedgerBounds `db:"ledger_bounds"`
MinAccountSequence null.Int `db:"min_account_sequence"`
MinAccountSequenceAge null.Int `db:"min_account_sequence_age"`
MinAccountSequenceLedgerGap null.Int `db:"min_account_sequence_ledger_gap"`
ExtraSigners pq.StringArray `db:"extra_signers"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
Successful bool `db:"successful"`
FeeAccount null.String `db:"fee_account"`
FeeAccountMuxed null.String `db:"fee_account_muxed"`
InnerTransactionHash null.String `db:"inner_transaction_hash"`
NewMaxFee null.Int `db:"new_max_fee"`
InnerSignatures pq.StringArray `db:"inner_signatures"`
}

func (i *transactionBatchInsertBuilder) transactionToRow(transaction ingest.LedgerTransaction, sequence uint32) (TransactionWithoutLedger, error) {
Expand All @@ -255,26 +172,32 @@ func (i *transactionBatchInsertBuilder) transactionToRow(transaction ingest.Ledg
if source.Type == xdr.CryptoKeyTypeKeyTypeMuxedEd25519 {
accountMuxed = null.StringFrom(source.Address())
}

t := TransactionWithoutLedger{
TransactionHash: hex.EncodeToString(transaction.Result.TransactionHash[:]),
LedgerSequence: int32(sequence),
ApplicationOrder: int32(transaction.Index),
Account: account.Address(),
AccountMuxed: accountMuxed,
AccountSequence: strconv.FormatInt(transaction.Envelope.SeqNum(), 10),
MaxFee: int64(transaction.Envelope.Fee()),
FeeCharged: int64(transaction.Result.Result.FeeCharged),
OperationCount: int32(len(transaction.Envelope.Operations())),
TxEnvelope: envelopeBase64,
TxResult: resultBase64,
TxMeta: metaBase64,
TxFeeMeta: feeMetaBase64,
TimeBounds: formatTimeBounds(transaction),
MemoType: memoType(transaction),
Memo: memo(transaction),
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
Successful: transaction.Result.Successful(),
TransactionHash: hex.EncodeToString(transaction.Result.TransactionHash[:]),
LedgerSequence: int32(sequence),
ApplicationOrder: int32(transaction.Index),
Account: account.Address(),
AccountMuxed: accountMuxed,
AccountSequence: strconv.FormatInt(transaction.Envelope.SeqNum(), 10),
MaxFee: int64(transaction.Envelope.Fee()),
FeeCharged: int64(transaction.Result.Result.FeeCharged),
OperationCount: int32(len(transaction.Envelope.Operations())),
TxEnvelope: envelopeBase64,
TxResult: resultBase64,
TxMeta: metaBase64,
TxFeeMeta: feeMetaBase64,
TimeBounds: formatTimeBounds(transaction.Envelope.TimeBounds()),
LedgerBounds: formatLedgerBounds(transaction.Envelope.LedgerBounds()),
MinAccountSequence: formatMinSequenceNumber(transaction.Envelope.MinSeqNum()),
MinAccountSequenceAge: formatDuration(transaction.Envelope.MinSeqAge()),
MinAccountSequenceLedgerGap: formatUint32(transaction.Envelope.MinSeqLedgerGap()),
ExtraSigners: formatSigners(transaction.Envelope.ExtraSigners()),
MemoType: memoType(transaction),
Memo: memo(transaction),
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
Successful: transaction.Result.Successful(),
}
t.TotalOrderID.ID = toid.New(int32(sequence), int32(transaction.Index), 0).ToInt64()

Expand Down Expand Up @@ -303,3 +226,32 @@ func (i *transactionBatchInsertBuilder) transactionToRow(transaction ingest.Ledg

return t, nil
}

func formatMinSequenceNumber(minSeqNum *int64) null.Int {
if minSeqNum == nil {
return null.Int{}
}
return null.IntFrom(int64(*minSeqNum))
}

func formatDuration(d *xdr.Duration) null.Int {
if d == nil {
return null.Int{}
}
return null.IntFrom(int64(*d))
}

func formatUint32(u *xdr.Uint32) null.Int {
if u == nil {
return null.Int{}
}
return null.IntFrom(int64(*u))
}

func formatSigners(s []xdr.SignerKey) pq.StringArray {
signers := make([]string, len(s))
for i, key := range s {
signers[i] = key.Address()
}
return signers
}
Loading