-
Notifications
You must be signed in to change notification settings - Fork 6
feat: track witness set for transaction #1027
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
base: main
Are you sure you want to change the base?
Changes from 4 commits
eb6ab54
6d6530d
c3fa7a0
dab5d82
6f5b11b
04852d0
3260f67
ffb53dc
d351aa8
d8edaf6
15ebae2
1c48d9a
f19efc1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| // Copyright 2025 Blink Labs Software | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package models | ||
|
|
||
| // Redeemer represents a redeemer in the witness set | ||
| type Redeemer struct { | ||
| ID uint `gorm:"primaryKey"` | ||
| TransactionID uint `gorm:"index"` | ||
| Tag uint8 `gorm:"index"` // Redeemer tag | ||
| Index uint32 `gorm:"index"` | ||
| Data []byte `gorm:"type:bytea"` // Plutus data | ||
|
||
| ExUnitsMemory uint64 | ||
| ExUnitsCPU uint64 | ||
| Transaction *Transaction | ||
|
||
| } | ||
|
|
||
| func (Redeemer) TableName() string { | ||
| return "redeemer" | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rename this file to |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| // Copyright 2025 Blink Labs Software | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package models | ||
|
|
||
| // Script represents a script entry in the witness set | ||
| // Type corresponds to ScriptRefType constants from gouroboros/ledger/common: | ||
| // 0=NativeScript (ScriptRefTypeNativeScript) | ||
| // 1=PlutusV1 (ScriptRefTypePlutusV1) | ||
| // 2=PlutusV2 (ScriptRefTypePlutusV2) | ||
| // 3=PlutusV3 (ScriptRefTypePlutusV3) | ||
| type Script struct { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rename this to |
||
| ID uint `gorm:"primaryKey"` | ||
| TransactionID uint `gorm:"index"` | ||
| Type uint8 `gorm:"index"` // Script type | ||
| ScriptData []byte `gorm:"type:bytea"` | ||
|
||
| Transaction *Transaction | ||
|
||
| } | ||
agaffney marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| func (Script) TableName() string { | ||
| return "script" | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| // Copyright 2025 Blink Labs Software | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package models | ||
|
|
||
| // KeyWitness represents a key witness entry (Vkey or Bootstrap) | ||
| // Type: 0 = VkeyWitness, 1 = BootstrapWitness | ||
|
||
| type KeyWitness struct { | ||
| ID uint `gorm:"primaryKey"` | ||
| TransactionID uint `gorm:"index"` | ||
| Type uint8 `gorm:"index"` // 0=Vkey, 1=Bootstrap | ||
| Vkey []byte `gorm:"type:bytea"` | ||
| Signature []byte `gorm:"type:bytea"` | ||
| PublicKey []byte `gorm:"type:bytea"` // For Bootstrap | ||
| ChainCode []byte `gorm:"type:bytea"` // For Bootstrap | ||
| Attributes []byte `gorm:"type:bytea"` // For Bootstrap | ||
| Transaction *Transaction | ||
|
||
| } | ||
|
|
||
| func (KeyWitness) TableName() string { | ||
| return "key_witness" | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -89,6 +89,17 @@ func (d *MetadataStoreSqlite) SetTransaction( | |
| if result.Error != nil { | ||
| return fmt.Errorf("create transaction: %w", result.Error) | ||
| } | ||
| // If ID is still zero (conflict path with SQLite), fetch it by hash | ||
| if tmpTx.ID == 0 { | ||
| existingTx, err := d.GetTransactionByHash(txHash, txn) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to fetch transaction ID after upsert: %w", err) | ||
| } | ||
| if existingTx == nil { | ||
| return fmt.Errorf("transaction not found after upsert: %x", txHash) | ||
| } | ||
| tmpTx.ID = existingTx.ID | ||
| } | ||
agaffney marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // Add Inputs to Transaction | ||
| for _, input := range tx.Inputs() { | ||
| inTxId := input.Id().Bytes() | ||
|
|
@@ -277,6 +288,130 @@ func (d *MetadataStoreSqlite) SetTransaction( | |
| return result.Error | ||
| } | ||
| } | ||
| // Extract and save witness set data | ||
| // Delete existing witness records to ensure idempotency on retry | ||
| if tmpTx.ID != 0 { | ||
|
||
| if result := txn.Where("transaction_id = ?", tmpTx.ID).Delete(&models.KeyWitness{}); result.Error != nil { | ||
| return fmt.Errorf("delete existing key witnesses: %w", result.Error) | ||
| } | ||
| if result := txn.Where("transaction_id = ?", tmpTx.ID).Delete(&models.Script{}); result.Error != nil { | ||
|
||
| return fmt.Errorf("delete existing scripts: %w", result.Error) | ||
| } | ||
| if result := txn.Where("transaction_id = ?", tmpTx.ID).Delete(&models.Redeemer{}); result.Error != nil { | ||
| return fmt.Errorf("delete existing redeemers: %w", result.Error) | ||
| } | ||
| if result := txn.Where("transaction_id = ?", tmpTx.ID).Delete(&models.PlutusData{}); result.Error != nil { | ||
| return fmt.Errorf("delete existing plutus data: %w", result.Error) | ||
| } | ||
| } | ||
agaffney marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if tx.Witnesses() != nil { | ||
| ws := tx.Witnesses() | ||
|
||
|
|
||
| // Add Vkey Witnesses | ||
| for _, vkey := range ws.Vkey() { | ||
| keyWitness := models.KeyWitness{ | ||
| TransactionID: tmpTx.ID, | ||
| Type: 0, // VkeyWitness | ||
|
||
| Vkey: vkey.Vkey, | ||
| Signature: vkey.Signature, | ||
| } | ||
| if result := txn.Create(&keyWitness); result.Error != nil { | ||
| return fmt.Errorf("create vkey witness: %w", result.Error) | ||
| } | ||
| } | ||
|
|
||
| // Add Bootstrap Witnesses | ||
| for _, bootstrap := range ws.Bootstrap() { | ||
| keyWitness := models.KeyWitness{ | ||
| TransactionID: tmpTx.ID, | ||
| Type: 1, // BootstrapWitness | ||
|
||
| PublicKey: bootstrap.PublicKey, | ||
| Signature: bootstrap.Signature, | ||
| ChainCode: bootstrap.ChainCode, | ||
| Attributes: bootstrap.Attributes, | ||
| } | ||
| if result := txn.Create(&keyWitness); result.Error != nil { | ||
| return fmt.Errorf("create bootstrap witness: %w", result.Error) | ||
| } | ||
| } | ||
|
|
||
| // Add Native Scripts | ||
| for _, script := range ws.NativeScripts() { | ||
| scriptRecord := models.Script{ | ||
| TransactionID: tmpTx.ID, | ||
| Type: uint8(lcommon.ScriptRefTypeNativeScript), | ||
| ScriptData: script.Cbor(), | ||
| } | ||
| if result := txn.Create(&scriptRecord); result.Error != nil { | ||
| return fmt.Errorf("create native script: %w", result.Error) | ||
| } | ||
|
||
| } | ||
|
|
||
| // Add PlutusV1 Scripts | ||
| for _, script := range ws.PlutusV1Scripts() { | ||
| scriptRecord := models.Script{ | ||
| TransactionID: tmpTx.ID, | ||
| Type: uint8(lcommon.ScriptRefTypePlutusV1), | ||
| ScriptData: script, | ||
| } | ||
| if result := txn.Create(&scriptRecord); result.Error != nil { | ||
| return fmt.Errorf("create plutus v1 script: %w", result.Error) | ||
| } | ||
| } | ||
|
|
||
| // Add PlutusV2 Scripts | ||
| for _, script := range ws.PlutusV2Scripts() { | ||
| scriptRecord := models.Script{ | ||
| TransactionID: tmpTx.ID, | ||
| Type: uint8(lcommon.ScriptRefTypePlutusV2), | ||
| ScriptData: script, | ||
| } | ||
| if result := txn.Create(&scriptRecord); result.Error != nil { | ||
| return fmt.Errorf("create plutus v2 script: %w", result.Error) | ||
| } | ||
| } | ||
|
|
||
| // Add PlutusV3 Scripts | ||
| for _, script := range ws.PlutusV3Scripts() { | ||
| scriptRecord := models.Script{ | ||
| TransactionID: tmpTx.ID, | ||
| Type: uint8(lcommon.ScriptRefTypePlutusV3), | ||
| ScriptData: script, | ||
| } | ||
| if result := txn.Create(&scriptRecord); result.Error != nil { | ||
| return fmt.Errorf("create plutus v3 script: %w", result.Error) | ||
| } | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could de-duplicate the above 4 blocks by processing them all at once: |
||
|
|
||
| // Add PlutusData (Datums) | ||
| for _, datum := range ws.PlutusData() { | ||
| plutusData := models.PlutusData{ | ||
| TransactionID: tmpTx.ID, | ||
| Data: datum.Cbor(), | ||
| } | ||
| if result := txn.Create(&plutusData); result.Error != nil { | ||
| return fmt.Errorf("create plutus data: %w", result.Error) | ||
| } | ||
| } | ||
|
|
||
| // Add Redeemers | ||
| if ws.Redeemers() != nil { | ||
| for key, value := range ws.Redeemers().Iter() { | ||
| redeemer := models.Redeemer{ | ||
| TransactionID: tmpTx.ID, | ||
| Tag: uint8(key.Tag), | ||
| Index: key.Index, | ||
| Data: value.Data.Cbor(), | ||
| ExUnitsMemory: uint64(max(0, value.ExUnits.Memory)), //nolint:gosec | ||
| ExUnitsCPU: uint64(max(0, value.ExUnits.Steps)), //nolint:gosec | ||
| } | ||
| if result := txn.Create(&redeemer); result.Error != nil { | ||
| return fmt.Errorf("create redeemer: %w", result.Error) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Avoid updating associations | ||
| result = txn.Omit(clause.Associations).Save(&tmpTx) | ||
| if result.Error != nil { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove the GORM field type override. This is a Postgres column type that won't work in SQLite