Skip to content

Commit 9f8cb4f

Browse files
authored
feat(ledger): genesis stake pools + delegations (#1432)
Signed-off-by: Chris Gianelloni <wolf31o2@blinklabs.io>
1 parent ce6040d commit 9f8cb4f

File tree

7 files changed

+250
-3
lines changed

7 files changed

+250
-3
lines changed

database/plugin/metadata/mysql/transaction.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1682,3 +1682,13 @@ func (d *MetadataStoreMysql) DeleteTransactionsAfterSlot(
16821682

16831683
return nil
16841684
}
1685+
1686+
// SetGenesisStaking is not implemented for the MySQL metadata plugin.
1687+
func (d *MetadataStoreMysql) SetGenesisStaking(
1688+
_ map[string]lcommon.PoolRegistrationCertificate,
1689+
_ map[string]string,
1690+
_ []byte,
1691+
_ types.Txn,
1692+
) error {
1693+
return errors.New("genesis staking not implemented for mysql")
1694+
}

database/plugin/metadata/postgres/transaction.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1660,3 +1660,13 @@ func (d *MetadataStorePostgres) DeleteTransactionsAfterSlot(
16601660

16611661
return nil
16621662
}
1663+
1664+
// SetGenesisStaking is not implemented for the PostgreSQL metadata plugin.
1665+
func (d *MetadataStorePostgres) SetGenesisStaking(
1666+
_ map[string]lcommon.PoolRegistrationCertificate,
1667+
_ map[string]string,
1668+
_ []byte,
1669+
_ types.Txn,
1670+
) error {
1671+
return errors.New("genesis staking not implemented for postgres")
1672+
}

database/plugin/metadata/sqlite/transaction.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package sqlite
1616

1717
import (
1818
"bytes"
19+
"encoding/hex"
1920
"errors"
2021
"fmt"
2122
"strings"
@@ -1654,6 +1655,166 @@ func (d *MetadataStoreSqlite) SetGenesisTransaction(
16541655
return nil
16551656
}
16561657

1658+
// SetGenesisStaking stores genesis pool registrations and stake delegations
1659+
// from the shelley-genesis.json staking section. It creates Pool,
1660+
// PoolRegistration, and Account records at slot 0.
1661+
func (d *MetadataStoreSqlite) SetGenesisStaking(
1662+
pools map[string]lcommon.PoolRegistrationCertificate,
1663+
stakeDelegations map[string]string,
1664+
blockHash []byte,
1665+
txn types.Txn,
1666+
) error {
1667+
db, err := d.resolveDB(txn)
1668+
if err != nil {
1669+
return err
1670+
}
1671+
1672+
// Batch fetch all existing pools to avoid N+1 queries
1673+
poolKeyHashes := make([][]byte, 0, len(pools))
1674+
for _, cert := range pools {
1675+
poolKeyHashes = append(poolKeyHashes, cert.Operator[:])
1676+
}
1677+
var existingPools []models.Pool
1678+
if len(poolKeyHashes) > 0 {
1679+
if result := db.Where(
1680+
"pool_key_hash IN ?",
1681+
poolKeyHashes,
1682+
).Find(&existingPools); result.Error != nil {
1683+
return fmt.Errorf(
1684+
"batch fetch genesis pools: %w",
1685+
result.Error,
1686+
)
1687+
}
1688+
}
1689+
existingPoolMap := make(map[string]*models.Pool, len(existingPools))
1690+
for i := range existingPools {
1691+
key := hex.EncodeToString(existingPools[i].PoolKeyHash)
1692+
existingPoolMap[key] = &existingPools[i]
1693+
}
1694+
1695+
for _, cert := range pools {
1696+
poolKey := hex.EncodeToString(cert.Operator[:])
1697+
tmpPool := existingPoolMap[poolKey]
1698+
if tmpPool == nil {
1699+
tmpPool = &models.Pool{
1700+
PoolKeyHash: cert.Operator[:],
1701+
VrfKeyHash: cert.VrfKeyHash[:],
1702+
}
1703+
}
1704+
tmpPool.Pledge = types.Uint64(cert.Pledge)
1705+
tmpPool.Cost = types.Uint64(cert.Cost)
1706+
tmpPool.Margin = &types.Rat{Rat: cert.Margin.Rat}
1707+
tmpPool.RewardAccount = cert.RewardAccount[:]
1708+
1709+
tmpReg := models.PoolRegistration{
1710+
PoolKeyHash: cert.Operator[:],
1711+
VrfKeyHash: cert.VrfKeyHash[:],
1712+
Pledge: types.Uint64(cert.Pledge),
1713+
Cost: types.Uint64(cert.Cost),
1714+
Margin: &types.Rat{Rat: cert.Margin.Rat},
1715+
RewardAccount: cert.RewardAccount[:],
1716+
AddedSlot: 0,
1717+
}
1718+
if cert.PoolMetadata != nil {
1719+
tmpReg.MetadataUrl = cert.PoolMetadata.Url
1720+
tmpReg.MetadataHash = cert.PoolMetadata.Hash[:]
1721+
}
1722+
for _, owner := range cert.PoolOwners {
1723+
tmpReg.Owners = append(
1724+
tmpReg.Owners,
1725+
models.PoolRegistrationOwner{KeyHash: owner[:]},
1726+
)
1727+
}
1728+
tmpPool.Owners = tmpReg.Owners
1729+
1730+
for _, relay := range cert.Relays {
1731+
tmpRelay := models.PoolRegistrationRelay{
1732+
Ipv4: relay.Ipv4,
1733+
Ipv6: relay.Ipv6,
1734+
}
1735+
if relay.Port != nil {
1736+
tmpRelay.Port = uint(*relay.Port)
1737+
}
1738+
if relay.Hostname != nil {
1739+
tmpRelay.Hostname = *relay.Hostname
1740+
}
1741+
tmpReg.Relays = append(tmpReg.Relays, tmpRelay)
1742+
}
1743+
tmpPool.Relays = tmpReg.Relays
1744+
1745+
if tmpPool.ID == 0 {
1746+
result := db.Omit(clause.Associations).Create(tmpPool)
1747+
if result.Error != nil {
1748+
return fmt.Errorf(
1749+
"create genesis pool: %w",
1750+
result.Error,
1751+
)
1752+
}
1753+
} else {
1754+
result := db.Omit(clause.Associations).Save(tmpPool)
1755+
if result.Error != nil {
1756+
return fmt.Errorf(
1757+
"save genesis pool: %w",
1758+
result.Error,
1759+
)
1760+
}
1761+
}
1762+
tmpReg.PoolID = tmpPool.ID
1763+
for i := range tmpReg.Owners {
1764+
tmpReg.Owners[i].PoolID = tmpPool.ID
1765+
}
1766+
for i := range tmpReg.Relays {
1767+
tmpReg.Relays[i].PoolID = tmpPool.ID
1768+
}
1769+
1770+
result := db.Create(&tmpReg)
1771+
if result.Error != nil {
1772+
return fmt.Errorf(
1773+
"create genesis pool registration: %w",
1774+
result.Error,
1775+
)
1776+
}
1777+
}
1778+
1779+
for stakerHex, poolHex := range stakeDelegations {
1780+
stakerBytes, err := hex.DecodeString(stakerHex)
1781+
if err != nil {
1782+
return fmt.Errorf(
1783+
"decode staker hash %s: %w",
1784+
stakerHex,
1785+
err,
1786+
)
1787+
}
1788+
poolBytes, err := hex.DecodeString(poolHex)
1789+
if err != nil {
1790+
return fmt.Errorf(
1791+
"decode pool hash %s: %w",
1792+
poolHex,
1793+
err,
1794+
)
1795+
}
1796+
1797+
account := &models.Account{
1798+
StakingKey: stakerBytes,
1799+
Pool: poolBytes,
1800+
Active: true,
1801+
AddedSlot: 0,
1802+
}
1803+
result := db.Clauses(clause.OnConflict{
1804+
Columns: []clause.Column{{Name: "staking_key"}},
1805+
DoNothing: true,
1806+
}).Create(account)
1807+
if result.Error != nil {
1808+
return fmt.Errorf(
1809+
"create genesis account: %w",
1810+
result.Error,
1811+
)
1812+
}
1813+
}
1814+
1815+
return nil
1816+
}
1817+
16571818
// Traverse each utxo and check for inline datum & calls storeDatum
16581819
func (d *MetadataStoreSqlite) storeTransactionDatums(
16591820
tx lcommon.Transaction,

database/plugin/metadata/store.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,17 @@ type MetadataStore interface {
279279
txn types.Txn,
280280
) error
281281

282+
// SetGenesisStaking stores genesis pool registrations and stake
283+
// delegations from the shelley-genesis.json staking section.
284+
// pools maps pool key hash (hex) to its registration certificate.
285+
// stakeDelegations maps staking credential hash (hex) to pool key hash (hex).
286+
SetGenesisStaking(
287+
pools map[string]lcommon.PoolRegistrationCertificate,
288+
stakeDelegations map[string]string,
289+
blockHash []byte,
290+
txn types.Txn,
291+
) error
292+
282293
// Helper methods
283294

284295
// DeleteBlockNoncesBeforeSlot removes block nonces older than the given slot.

database/transaction.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,36 @@ func (d *Database) SetGenesisTransaction(
221221
return nil
222222
}
223223

224+
// SetGenesisStaking stores genesis pool registrations and stake
225+
// delegations. This is metadata-only (no blob operations needed).
226+
func (d *Database) SetGenesisStaking(
227+
pools map[string]lcommon.PoolRegistrationCertificate,
228+
stakeDelegations map[string]string,
229+
blockHash []byte,
230+
txn *Txn,
231+
) error {
232+
if txn == nil {
233+
if err := d.metadata.SetGenesisStaking(
234+
pools,
235+
stakeDelegations,
236+
blockHash,
237+
nil,
238+
); err != nil {
239+
return fmt.Errorf("set genesis staking: %w", err)
240+
}
241+
return nil
242+
}
243+
if err := d.metadata.SetGenesisStaking(
244+
pools,
245+
stakeDelegations,
246+
blockHash,
247+
txn.Metadata(),
248+
); err != nil {
249+
return fmt.Errorf("set genesis staking: %w", err)
250+
}
251+
return nil
252+
}
253+
224254
func (d *Database) GetTransactionByHash(
225255
hash []byte,
226256
txn *Txn,

ledger/chainsync.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,31 @@ func (ls *LedgerState) createGenesisBlock() error {
623623
"component", "ledger",
624624
)
625625

626+
// Load genesis staking data (pool registrations + delegations)
627+
genesisPools, _, err := shelleyGenesis.InitialPools()
628+
if err != nil {
629+
return fmt.Errorf("parse genesis staking: %w", err)
630+
}
631+
if len(genesisPools) > 0 ||
632+
len(shelleyGenesis.Staking.Stake) > 0 {
633+
ls.config.Logger.Info(
634+
fmt.Sprintf(
635+
"loading genesis staking: %d pools, %d delegations",
636+
len(genesisPools),
637+
len(shelleyGenesis.Staking.Stake),
638+
),
639+
"component", "ledger",
640+
)
641+
if err := ls.db.SetGenesisStaking(
642+
genesisPools,
643+
shelleyGenesis.Staking.Stake,
644+
genesisHash[:],
645+
txn,
646+
); err != nil {
647+
return fmt.Errorf("set genesis staking: %w", err)
648+
}
649+
}
650+
626651
return nil
627652
})
628653
return err

ledger/snapshot/calculator_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func seedPoolAndDelegations(
5858
sqliteStore *sqlite.MetadataStoreSqlite,
5959
poolKeyHash []byte,
6060
delegations []struct {
61-
stakingKey []byte
61+
stakingKey []byte
6262
utxoAmounts []types.Uint64
6363
},
6464
slot uint64,
@@ -325,7 +325,7 @@ func TestCalculateStakeDistribution_InactiveAccountsExcluded(t *testing.T) {
325325
StakingKey: activeKey, Pool: poolHash, AddedSlot: 100, Active: true,
326326
}).Error)
327327
require.NoError(t, gormDB.Create(&models.Utxo{
328-
TxId: []byte("tx_activ_567890123456789012345678901234"),
328+
TxId: []byte("tx_activ_567890123456789012345678901234"),
329329
OutputIdx: 0, StakingKey: activeKey,
330330
Amount: 7000000, AddedSlot: 100,
331331
}).Error)
@@ -340,7 +340,7 @@ func TestCalculateStakeDistribution_InactiveAccountsExcluded(t *testing.T) {
340340
require.NoError(t, gormDB.Create(&inactiveAcct).Error)
341341
require.NoError(t, gormDB.Model(&inactiveAcct).Update("active", false).Error)
342342
require.NoError(t, gormDB.Create(&models.Utxo{
343-
TxId: []byte("tx_inact_567890123456789012345678901234"),
343+
TxId: []byte("tx_inact_567890123456789012345678901234"),
344344
OutputIdx: 0, StakingKey: inactiveKey,
345345
Amount: 15000000, AddedSlot: 100,
346346
}).Error)

0 commit comments

Comments
 (0)