Skip to content

Commit

Permalink
services/horizon: Support new CAP-21 transaction conditions (#4297)
Browse files Browse the repository at this point in the history
* Add cap-21 transaction preconditions

* Replace xdr.TransactionEnvelope.TimeBounds -> .Preconditions function

* Rename Signers -> ExtraSigners for more consistent naming

* Store the new precondition fields in the DB

* Render the new precondition fields in the resourceadapter

* ./gogenerate.sh

* use a pointer so omitempty works

* more consistent naming

* Keep Timebounds helper

* PR feedback

* tidy transaction precondition helpers

* typo fixes

* remove dead param

* use bigint so we have enough space for a uint32

* use text instead of varchar, to avoid future migrations

* Updating scenario sqls

* need to load the preconditions from the db

* Temporary passing data for v2 preconditions transaction insertion test

* Update xdr/transaction_envelope_test.go

Co-authored-by: George <[email protected]>

* Update services/horizon/internal/db2/history/transaction_ledger_bounds.go

Co-authored-by: George <[email protected]>

* PR feedback

Co-authored-by: George <[email protected]>
  • Loading branch information
Paul Bellamy and Shaptic authored Mar 25, 2022
1 parent 649016c commit 9fdd7ff
Show file tree
Hide file tree
Showing 29 changed files with 1,262 additions and 434 deletions.
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

0 comments on commit 9fdd7ff

Please sign in to comment.